@dastageer_44/eco-code 1.1.0 → 1.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/README.md +76 -69
- package/dist/config.d.ts +7 -1
- package/dist/config.js +62 -4
- package/dist/config.js.map +1 -1
- package/dist/harness/agent.d.ts +9 -2
- package/dist/harness/agent.js +69 -20
- package/dist/harness/agent.js.map +1 -1
- package/dist/harness/context.d.ts +6 -1
- package/dist/harness/context.js +57 -6
- package/dist/harness/context.js.map +1 -1
- package/dist/index.js +433 -161
- package/dist/index.js.map +1 -1
- package/dist/services/updater.d.ts +4 -0
- package/dist/services/updater.js +46 -2
- package/dist/services/updater.js.map +1 -1
- package/dist/tools/file-ops.js +7 -0
- package/dist/tools/file-ops.js.map +1 -1
- package/dist/tools/index.js +14 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/ui/banner.d.ts +1 -1
- package/dist/ui/banner.js +20 -11
- package/dist/ui/banner.js.map +1 -1
- package/dist/ui/markdown.d.ts +1 -0
- package/dist/ui/markdown.js +78 -0
- package/dist/ui/markdown.js.map +1 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -12,12 +12,14 @@ import * as readline from 'readline';
|
|
|
12
12
|
import * as p from '@clack/prompts';
|
|
13
13
|
import chalk from 'chalk';
|
|
14
14
|
import ora from 'ora';
|
|
15
|
+
import OpenAI from 'openai';
|
|
15
16
|
import { ConfigManager } from './config.js';
|
|
16
17
|
import { OpenRouterService } from './services/openrouter.js';
|
|
17
18
|
import { AgentHarness } from './harness/agent.js';
|
|
18
19
|
import { printBanner, printHelp } from './ui/banner.js';
|
|
20
|
+
import { renderMarkdown } from './ui/markdown.js';
|
|
19
21
|
import { runCommand } from './tools/shell-ops.js';
|
|
20
|
-
import { checkAndPromptUpdate } from './services/updater.js';
|
|
22
|
+
import { checkAndPromptUpdate, triggerManualUpdate } from './services/updater.js';
|
|
21
23
|
const SPINNER_VERBS = [
|
|
22
24
|
'Thinking', 'Reasoning', 'Analyzing', 'Synthesizing',
|
|
23
25
|
'Drafting', 'Evaluating', 'Planning', 'Processing'
|
|
@@ -30,89 +32,243 @@ function getPackageVersion() {
|
|
|
30
32
|
const pkgPath = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')), '../package.json');
|
|
31
33
|
if (fs.existsSync(pkgPath)) {
|
|
32
34
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
33
|
-
return pkg.version || '1.
|
|
35
|
+
return pkg.version || '1.2.0';
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
catch { }
|
|
37
|
-
return '1.
|
|
39
|
+
return '1.2.0';
|
|
38
40
|
}
|
|
39
41
|
const config = new ConfigManager();
|
|
40
42
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
+
* Antigravity-style Interactive Terminal Prompt
|
|
44
|
+
* Listens to raw keypress events immediately on Windows, macOS, and Linux.
|
|
45
|
+
* When the user types '/', it pops open a live autocomplete dropdown directly under the prompt line.
|
|
43
46
|
*/
|
|
44
47
|
class PromptManager {
|
|
45
|
-
|
|
48
|
+
sigintCount = 0;
|
|
49
|
+
isPaused = false;
|
|
46
50
|
constructor() {
|
|
47
|
-
|
|
48
|
-
input: process.stdin,
|
|
49
|
-
output: process.stdout,
|
|
50
|
-
terminal: true,
|
|
51
|
-
});
|
|
51
|
+
readline.emitKeypressEvents(process.stdin);
|
|
52
52
|
}
|
|
53
|
-
ask(
|
|
53
|
+
ask(promptPrefix, getStats) {
|
|
54
54
|
return new Promise((resolve) => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
const stats = getStats ? getStats() : null;
|
|
56
|
+
let compactDesc = 'Compact conversation history';
|
|
57
|
+
if (stats) {
|
|
58
|
+
compactDesc = `Compact history ${chalk.dim(stats.progressBar)} ${chalk.green(`${stats.percentage}% used`)}`;
|
|
59
|
+
if (stats.isCritical) {
|
|
60
|
+
compactDesc = `Compact history ${chalk.red.bold(stats.progressBar)} ${chalk.red.bold(`${stats.percentage}% used (⚠️ Recommended)`)}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const allCommands = [
|
|
64
|
+
{ value: '/plan', name: '/plan', desc: '📝 Toggle between Plan Mode and Auto Mode' },
|
|
65
|
+
{ value: '/models', name: '/models', desc: 'Browse & switch free AI models' },
|
|
66
|
+
{ value: '/compact', name: '/compact', desc: compactDesc },
|
|
67
|
+
{ value: '/resume', name: '/resume', desc: 'Restore previous session context' },
|
|
68
|
+
{ value: '/undo', name: '/undo', desc: '🗑️ Discard all uncommitted code changes' },
|
|
69
|
+
{ value: '/rewind', name: '/rewind', desc: '⏪ Undo the last Git commit (git reset HEAD~1)' },
|
|
70
|
+
{ value: '/refresh', name: '/refresh', desc: 'Check for latest free models' },
|
|
71
|
+
{ value: '/taste', name: '/taste', desc: 'View or add project coding style rules' },
|
|
72
|
+
{ value: '/key', name: '/key', desc: 'Update OpenRouter API Key' },
|
|
73
|
+
{ value: '/clear', name: '/clear', desc: 'Reset conversation history' },
|
|
74
|
+
{ value: '/update', name: '/update', desc: '🔄 Check for and install CLI updates' },
|
|
75
|
+
{ value: '/help', name: '/help', desc: 'Show help summary' },
|
|
76
|
+
{ value: '/exit', name: '/exit', desc: 'Quit the agent' },
|
|
77
|
+
];
|
|
78
|
+
const wasRaw = process.stdin.isRaw;
|
|
79
|
+
if (process.stdin.setRawMode)
|
|
80
|
+
process.stdin.setRawMode(true);
|
|
81
|
+
process.stdin.resume();
|
|
82
|
+
let buffer = '';
|
|
83
|
+
let cursorPos = 0;
|
|
84
|
+
let selectedIndex = 0;
|
|
85
|
+
let renderedDropdownLines = 0;
|
|
86
|
+
const render = () => {
|
|
87
|
+
if (this.isPaused)
|
|
88
|
+
return;
|
|
89
|
+
// 1. Erase previously rendered dropdown lines below
|
|
90
|
+
if (renderedDropdownLines > 0) {
|
|
62
91
|
readline.cursorTo(process.stdout, 0);
|
|
63
|
-
|
|
64
|
-
|
|
92
|
+
for (let i = 0; i < renderedDropdownLines; i++) {
|
|
93
|
+
process.stdout.write('\x1b[1B\x1b[2K');
|
|
94
|
+
}
|
|
95
|
+
process.stdout.write(`\x1b[${renderedDropdownLines}A`);
|
|
65
96
|
}
|
|
97
|
+
// 2. Erase prompt line and re-render prompt + buffer
|
|
98
|
+
readline.cursorTo(process.stdout, 0);
|
|
99
|
+
process.stdout.write('\x1b[2K');
|
|
100
|
+
process.stdout.write(promptPrefix + buffer);
|
|
101
|
+
renderedDropdownLines = 0;
|
|
102
|
+
// 3. If buffer starts with '/', render live dropdown beneath
|
|
103
|
+
if (buffer.startsWith('/')) {
|
|
104
|
+
const query = buffer.slice(1).toLowerCase().trim();
|
|
105
|
+
const filtered = allCommands.filter((c) => c.name.toLowerCase().includes(query) || c.desc.toLowerCase().includes(query));
|
|
106
|
+
if (selectedIndex >= filtered.length) {
|
|
107
|
+
selectedIndex = Math.max(0, filtered.length - 1);
|
|
108
|
+
}
|
|
109
|
+
const lines = [];
|
|
110
|
+
lines.push(` ${chalk.dim('─'.repeat(55))}`);
|
|
111
|
+
if (filtered.length === 0) {
|
|
112
|
+
lines.push(` ${chalk.dim('(no matching commands)')}`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
filtered.slice(0, 7).forEach((cmd, idx) => {
|
|
116
|
+
const isSelected = idx === selectedIndex;
|
|
117
|
+
const pointer = isSelected ? chalk.cyan('❯') : ' ';
|
|
118
|
+
const nameStr = isSelected ? chalk.cyan.bold(cmd.name.padEnd(10)) : chalk.white(cmd.name.padEnd(10));
|
|
119
|
+
const descStr = isSelected ? chalk.white(cmd.desc) : chalk.dim(cmd.desc);
|
|
120
|
+
lines.push(` ${pointer} ${nameStr} ${chalk.dim('—')} ${descStr}`);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
lines.push(` ${chalk.dim('↑/↓ Navigate · enter Select · tab Complete · esc to cancel')}`);
|
|
124
|
+
process.stdout.write('\n' + lines.join('\n'));
|
|
125
|
+
renderedDropdownLines = lines.length;
|
|
126
|
+
// Move cursor back up to prompt line
|
|
127
|
+
process.stdout.write(`\x1b[${renderedDropdownLines}A`);
|
|
128
|
+
}
|
|
129
|
+
// 4. Position cursor accurately on prompt line
|
|
130
|
+
const visiblePrompt = promptPrefix.replace(/\x1b\[[0-9;]*m/g, '');
|
|
131
|
+
readline.cursorTo(process.stdout, visiblePrompt.length + cursorPos);
|
|
66
132
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
133
|
+
const cleanup = () => {
|
|
134
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
135
|
+
if (renderedDropdownLines > 0) {
|
|
136
|
+
readline.cursorTo(process.stdout, 0);
|
|
137
|
+
for (let i = 0; i < renderedDropdownLines; i++) {
|
|
138
|
+
process.stdout.write('\x1b[1B\x1b[2K');
|
|
139
|
+
}
|
|
140
|
+
process.stdout.write(`\x1b[${renderedDropdownLines}A`);
|
|
141
|
+
readline.cursorTo(process.stdout, 0);
|
|
142
|
+
}
|
|
143
|
+
if (process.stdin.setRawMode && !wasRaw) {
|
|
144
|
+
process.stdin.setRawMode(false);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const onKeypress = (str, key) => {
|
|
148
|
+
if (this.isPaused)
|
|
149
|
+
return;
|
|
150
|
+
if (key) {
|
|
151
|
+
if (key.ctrl && key.name === 'c') {
|
|
152
|
+
this.sigintCount++;
|
|
153
|
+
if (this.sigintCount >= 3) {
|
|
154
|
+
cleanup();
|
|
155
|
+
console.log('\nHappy coding! 👋');
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
cleanup();
|
|
160
|
+
console.log(`\n${chalk.yellow(`(Press Ctrl+C ${3 - this.sigintCount} more time(s) to exit)`)}`);
|
|
161
|
+
process.stdout.write('\n');
|
|
162
|
+
resolve('');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
167
|
+
let finalVal = buffer.trim();
|
|
168
|
+
if (buffer.startsWith('/')) {
|
|
169
|
+
const query = buffer.slice(1).toLowerCase().trim();
|
|
170
|
+
const filtered = allCommands.filter((c) => c.name.toLowerCase().includes(query) || c.desc.toLowerCase().includes(query));
|
|
171
|
+
if (filtered.length > 0) {
|
|
172
|
+
finalVal = filtered[selectedIndex].value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
cleanup();
|
|
176
|
+
process.stdout.write('\n');
|
|
177
|
+
this.sigintCount = 0;
|
|
178
|
+
resolve(finalVal);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (key.name === 'up' && buffer.startsWith('/')) {
|
|
182
|
+
const query = buffer.slice(1).toLowerCase().trim();
|
|
183
|
+
const filtered = allCommands.filter((c) => c.name.toLowerCase().includes(query) || c.desc.toLowerCase().includes(query));
|
|
184
|
+
if (filtered.length > 0) {
|
|
185
|
+
selectedIndex = (selectedIndex - 1 + filtered.length) % filtered.length;
|
|
186
|
+
render();
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (key.name === 'down' && buffer.startsWith('/')) {
|
|
191
|
+
const query = buffer.slice(1).toLowerCase().trim();
|
|
192
|
+
const filtered = allCommands.filter((c) => c.name.toLowerCase().includes(query) || c.desc.toLowerCase().includes(query));
|
|
193
|
+
if (filtered.length > 0) {
|
|
194
|
+
selectedIndex = (selectedIndex + 1) % filtered.length;
|
|
195
|
+
render();
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (key.name === 'tab' && buffer.startsWith('/')) {
|
|
200
|
+
const query = buffer.slice(1).toLowerCase().trim();
|
|
201
|
+
const filtered = allCommands.filter((c) => c.name.toLowerCase().includes(query) || c.desc.toLowerCase().includes(query));
|
|
202
|
+
if (filtered.length > 0) {
|
|
203
|
+
buffer = filtered[selectedIndex].name;
|
|
204
|
+
cursorPos = buffer.length;
|
|
205
|
+
render();
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (key.name === 'escape') {
|
|
210
|
+
buffer = '';
|
|
211
|
+
cursorPos = 0;
|
|
212
|
+
selectedIndex = 0;
|
|
213
|
+
render();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (key.name === 'backspace') {
|
|
217
|
+
if (cursorPos > 0) {
|
|
218
|
+
buffer = buffer.slice(0, cursorPos - 1) + buffer.slice(cursorPos);
|
|
219
|
+
cursorPos--;
|
|
220
|
+
selectedIndex = 0;
|
|
221
|
+
render();
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (key.name === 'left') {
|
|
226
|
+
if (cursorPos > 0) {
|
|
227
|
+
cursorPos--;
|
|
228
|
+
render();
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (key.name === 'right') {
|
|
233
|
+
if (cursorPos < buffer.length) {
|
|
234
|
+
cursorPos++;
|
|
235
|
+
render();
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Handle normal characters
|
|
241
|
+
if (str && str.length === 1 && str.charCodeAt(0) >= 32) {
|
|
242
|
+
buffer = buffer.slice(0, cursorPos) + str + buffer.slice(cursorPos);
|
|
243
|
+
cursorPos += str.length;
|
|
244
|
+
selectedIndex = 0;
|
|
245
|
+
render();
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
process.stdin.on('keypress', onKeypress);
|
|
249
|
+
render();
|
|
72
250
|
});
|
|
73
251
|
}
|
|
74
252
|
pause() {
|
|
75
|
-
this.
|
|
253
|
+
this.isPaused = true;
|
|
254
|
+
if (process.stdin.setRawMode)
|
|
255
|
+
process.stdin.setRawMode(false);
|
|
76
256
|
}
|
|
77
257
|
resume() {
|
|
78
|
-
this.
|
|
79
|
-
}
|
|
80
|
-
close() {
|
|
81
|
-
this.rl.close();
|
|
258
|
+
this.isPaused = false;
|
|
82
259
|
}
|
|
260
|
+
close() { }
|
|
83
261
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
});
|
|
94
|
-
if (p.isCancel(method)) {
|
|
95
|
-
p.outro('Setup cancelled.');
|
|
96
|
-
process.exit(0);
|
|
97
|
-
}
|
|
98
|
-
if (method === 'env') {
|
|
99
|
-
p.note(`Windows (PowerShell):\n $env:OPENROUTER_API_KEY="sk-or-v1-..."\n\nLinux / macOS / Bash:\n export OPENROUTER_API_KEY="sk-or-v1-..."\n\nGet a free key at: https://openrouter.ai/keys`, 'Environment Variable Setup');
|
|
100
|
-
}
|
|
101
|
-
else if (method === 'config') {
|
|
102
|
-
p.note(`File location:\n ${config.getConfigPath()}\n\nContent:\n {\n "apiKey": "sk-or-v1-..."\n }`, 'Config File Setup');
|
|
103
|
-
}
|
|
104
|
-
const entered = await p.password({
|
|
105
|
-
message: 'Enter your OpenRouter API Key:',
|
|
106
|
-
validate: (val) => (!val || !val.trim() ? 'API key cannot be empty' : undefined),
|
|
107
|
-
});
|
|
108
|
-
if (p.isCancel(entered)) {
|
|
109
|
-
p.outro('Setup cancelled.');
|
|
110
|
-
process.exit(0);
|
|
111
|
-
}
|
|
112
|
-
const key = entered.trim();
|
|
113
|
-
config.setApiKey(key);
|
|
114
|
-
p.note(`Saved API key to: ${config.getConfigPath()}`, 'Key Saved');
|
|
115
|
-
return key;
|
|
262
|
+
function printMissingApiKeyInstructions() {
|
|
263
|
+
console.log(`\n${chalk.red.bold(' ❌ No OpenRouter API Key configured.')}\n`);
|
|
264
|
+
console.log(` To use ${chalk.green.bold('eco-code')}, set your OpenRouter API key:\n`);
|
|
265
|
+
console.log(` ${chalk.cyan.bold('Windows (PowerShell):')}`);
|
|
266
|
+
console.log(` ${chalk.white('$env:OPENROUTER_API_KEY="sk-or-v1-your-key-here"')}\n`);
|
|
267
|
+
console.log(` ${chalk.cyan.bold('Linux / macOS (Bash / Zsh):')}`);
|
|
268
|
+
console.log(` ${chalk.white('export OPENROUTER_API_KEY="sk-or-v1-your-key-here"')}\n`);
|
|
269
|
+
console.log(` ${chalk.cyan.bold('Or in ~/.eco-code/config.json:')}`);
|
|
270
|
+
console.log(` ${chalk.white('{\n "apiKey": "sk-or-v1-your-key-here"\n }')}\n`);
|
|
271
|
+
console.log(` ${chalk.dim('Get a free API key at:')} ${chalk.underline('https://openrouter.ai/keys')}\n`);
|
|
116
272
|
}
|
|
117
273
|
async function selectModelInteractive(models, currentModel) {
|
|
118
274
|
const options = models.slice(0, 15).map((m) => {
|
|
@@ -133,32 +289,6 @@ async function selectModelInteractive(models, currentModel) {
|
|
|
133
289
|
}
|
|
134
290
|
return models.find((m) => m.id === chosen) || models[0];
|
|
135
291
|
}
|
|
136
|
-
async function handleSlashCommandMenu(stats) {
|
|
137
|
-
let compactLabel = `/compact - Compact history ${chalk.dim(stats.progressBar)} ${chalk.green(`${stats.percentage}% used`)}`;
|
|
138
|
-
if (stats.isCritical) {
|
|
139
|
-
compactLabel = `/compact - Compact history ${chalk.red.bold(stats.progressBar)} ${chalk.red.bold(`${stats.percentage}% used (⚠️ Recommended now!)`)}`;
|
|
140
|
-
}
|
|
141
|
-
else if (stats.isWarning) {
|
|
142
|
-
compactLabel = `/compact - Compact history ${chalk.yellow(stats.progressBar)} ${chalk.yellow(`${stats.percentage}% used (Consider compacting)`)}`;
|
|
143
|
-
}
|
|
144
|
-
const action = await p.select({
|
|
145
|
-
message: 'Select command:',
|
|
146
|
-
options: [
|
|
147
|
-
{ value: '/compact', label: compactLabel },
|
|
148
|
-
{ value: '/models', label: '/models - Browse & switch free models' },
|
|
149
|
-
{ value: '/refresh', label: '/refresh - Check for latest free models' },
|
|
150
|
-
{ value: '/resume', label: '/resume - Restore previous session context' },
|
|
151
|
-
{ value: '/taste', label: '/taste - View or add project coding style rules' },
|
|
152
|
-
{ value: '/key', label: '/key - Update OpenRouter API Key' },
|
|
153
|
-
{ value: '/clear', label: '/clear - Reset conversation history' },
|
|
154
|
-
{ value: '/help', label: '/help - Show help summary' },
|
|
155
|
-
{ value: '/exit', label: '/exit - Quit the agent' },
|
|
156
|
-
],
|
|
157
|
-
});
|
|
158
|
-
if (p.isCancel(action))
|
|
159
|
-
return null;
|
|
160
|
-
return action;
|
|
161
|
-
}
|
|
162
292
|
function printContextUsageHint(stats) {
|
|
163
293
|
const usedK = (stats.usedTokens / 1000).toFixed(1);
|
|
164
294
|
const maxK = (stats.maxTokens / 1000).toFixed(1);
|
|
@@ -171,43 +301,52 @@ function printContextUsageHint(stats) {
|
|
|
171
301
|
}
|
|
172
302
|
async function main() {
|
|
173
303
|
const version = getPackageVersion();
|
|
174
|
-
printBanner(version);
|
|
175
304
|
// Auto-check for updates on launch
|
|
176
|
-
await checkAndPromptUpdate(version, 'eco-code');
|
|
305
|
+
await checkAndPromptUpdate(version, '@dastageer_44/eco-code');
|
|
177
306
|
let apiKey = config.getApiKey();
|
|
178
|
-
|
|
179
|
-
apiKey = await promptOnboardingApiKey();
|
|
180
|
-
}
|
|
181
|
-
let openRouter = new OpenRouterService(apiKey, config);
|
|
182
|
-
const spinner = ora({ text: 'Checking models...', color: 'green' }).start();
|
|
307
|
+
let openRouter = null;
|
|
183
308
|
let freeModels = [];
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
309
|
+
let currentModelInfo = null;
|
|
310
|
+
let activeModel = config.getDefaultModel() || 'openrouter/auto';
|
|
311
|
+
if (apiKey) {
|
|
312
|
+
openRouter = new OpenRouterService(apiKey, config);
|
|
313
|
+
const spinner = ora({ text: 'Checking models...', color: 'green' }).start();
|
|
314
|
+
try {
|
|
315
|
+
const result = await openRouter.getOrFetchFreeModels(false);
|
|
316
|
+
freeModels = result.models;
|
|
317
|
+
if (freeModels.length > 0) {
|
|
318
|
+
currentModelInfo = freeModels.find((m) => m.id === config.getDefaultModel()) || freeModels[0];
|
|
319
|
+
activeModel = currentModelInfo.id;
|
|
320
|
+
config.setDefaultModel(activeModel);
|
|
321
|
+
}
|
|
322
|
+
spinner.stop();
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
spinner.fail('Failed to connect to OpenRouter.');
|
|
326
|
+
console.error(chalk.red(err.message));
|
|
191
327
|
}
|
|
192
|
-
spinner.succeed(chalk.green('Models updated'));
|
|
193
|
-
}
|
|
194
|
-
catch (err) {
|
|
195
|
-
spinner.fail('Failed to connect to OpenRouter.');
|
|
196
|
-
console.error(chalk.red(err.message));
|
|
197
|
-
console.log(chalk.yellow(`\nCheck your API key in ${config.getConfigPath()} or run /key.`));
|
|
198
|
-
process.exit(1);
|
|
199
328
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
329
|
+
const modelDisplay = apiKey && currentModelInfo
|
|
330
|
+
? `${activeModel} (${currentModelInfo.contextLength.toLocaleString()} tokens)`
|
|
331
|
+
: chalk.yellow('No API Key (type /key to configure)');
|
|
332
|
+
printBanner(version, modelDisplay, process.cwd());
|
|
333
|
+
const initialClient = openRouter
|
|
334
|
+
? openRouter.getClient()
|
|
335
|
+
: new OpenAI({ apiKey: 'unconfigured', baseURL: 'https://openrouter.ai/api/v1' });
|
|
336
|
+
const harness = new AgentHarness(initialClient, activeModel, currentModelInfo?.contextLength || 8192, config, process.cwd());
|
|
206
337
|
const promptMgr = new PromptManager();
|
|
338
|
+
// Force Plan Mode on startup
|
|
339
|
+
config.setAgentMode('plan');
|
|
340
|
+
harness.updateSystemPrompt('plan');
|
|
341
|
+
if (!apiKey) {
|
|
342
|
+
console.log(chalk.yellow(` ⚠️ No OpenRouter API key detected. Type ${chalk.cyan.bold('/key')} or set ${chalk.cyan('$env:OPENROUTER_API_KEY')} to start.\n`));
|
|
343
|
+
}
|
|
207
344
|
// Main interactive loop
|
|
208
345
|
while (true) {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
346
|
+
const currentMode = config.getAgentMode().toUpperCase();
|
|
347
|
+
const modeColor = currentMode === 'PLAN' ? chalk.blue : chalk.red;
|
|
348
|
+
const promptStr = `${chalk.white.bold('eco-code')} ${modeColor(`[${currentMode}]`)} ${chalk.gray('>')} `;
|
|
349
|
+
const rawInput = await promptMgr.ask(promptStr, () => harness.getContextStats());
|
|
211
350
|
if (rawInput === null || rawInput === undefined) {
|
|
212
351
|
console.log('\nHappy coding! 👋');
|
|
213
352
|
promptMgr.close();
|
|
@@ -216,29 +355,23 @@ async function main() {
|
|
|
216
355
|
let prompt = rawInput.trim();
|
|
217
356
|
if (!prompt)
|
|
218
357
|
continue;
|
|
219
|
-
// INSTANT TRIGGER: If user pressed '/' or typed '/menu'
|
|
220
|
-
if (prompt === '/' || prompt === '/menu') {
|
|
221
|
-
promptMgr.pause();
|
|
222
|
-
const stats = harness.getContextStats();
|
|
223
|
-
const chosenCmd = await handleSlashCommandMenu(stats);
|
|
224
|
-
promptMgr.resume();
|
|
225
|
-
if (!chosenCmd)
|
|
226
|
-
continue;
|
|
227
|
-
prompt = chosenCmd;
|
|
228
|
-
}
|
|
229
358
|
// --- Slash Commands Execution ---
|
|
230
359
|
if (prompt === '/exit' || prompt === 'exit' || prompt === 'quit') {
|
|
231
360
|
console.log('\nHappy coding! 👋\n');
|
|
232
361
|
promptMgr.close();
|
|
233
362
|
process.exit(0);
|
|
234
363
|
}
|
|
235
|
-
if (prompt === '/help') {
|
|
364
|
+
if (prompt === '/help' || prompt === '?' || prompt === 'help') {
|
|
236
365
|
printHelp();
|
|
237
366
|
const stats = harness.getContextStats();
|
|
238
367
|
console.log(` ${chalk.bold('Current Context:')} ${stats.progressBar} ${chalk.green(`${stats.percentage}%`)} (${(stats.usedTokens / 1000).toFixed(1)}k / ${(stats.maxTokens / 1000).toFixed(1)}k tokens)\n`);
|
|
239
368
|
continue;
|
|
240
369
|
}
|
|
241
370
|
if (prompt === '/model' || prompt === '/models') {
|
|
371
|
+
if (!apiKey || freeModels.length === 0) {
|
|
372
|
+
console.log(chalk.yellow('\n ⚠️ No active API key. Type /key or set $env:OPENROUTER_API_KEY to browse free models.\n'));
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
242
375
|
promptMgr.pause();
|
|
243
376
|
const selected = await selectModelInteractive(freeModels, activeModel);
|
|
244
377
|
promptMgr.resume();
|
|
@@ -250,6 +383,10 @@ async function main() {
|
|
|
250
383
|
continue;
|
|
251
384
|
}
|
|
252
385
|
if (prompt === '/refresh') {
|
|
386
|
+
if (!apiKey || !openRouter) {
|
|
387
|
+
console.log(chalk.yellow('\n ⚠️ No active API key. Type /key or set $env:OPENROUTER_API_KEY first.\n'));
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
253
390
|
const refSpinner = ora('Checking for latest free models...').start();
|
|
254
391
|
try {
|
|
255
392
|
const res = await openRouter.getOrFetchFreeModels(true);
|
|
@@ -278,21 +415,47 @@ async function main() {
|
|
|
278
415
|
continue;
|
|
279
416
|
}
|
|
280
417
|
if (prompt === '/resume') {
|
|
281
|
-
const
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
418
|
+
const sessions = config.listSessions();
|
|
419
|
+
if (!sessions || sessions.length === 0) {
|
|
420
|
+
console.log(`\n ${chalk.yellow('ℹ')} No previous sessions found to resume.\n`);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
let selectedSession = null;
|
|
424
|
+
if (sessions.length === 1) {
|
|
425
|
+
selectedSession = sessions[0];
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
promptMgr.pause();
|
|
429
|
+
const options = sessions.slice(0, 10).map((s) => {
|
|
430
|
+
const d = new Date(s.timestamp);
|
|
431
|
+
const dateStr = d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
432
|
+
const title = s.title || `Session with ${s.messages.length} messages`;
|
|
433
|
+
return {
|
|
434
|
+
value: s.id,
|
|
435
|
+
label: `${chalk.cyan(dateStr)} — ${chalk.white(title)} ${chalk.dim(`(${s.model || 'unknown'})`)}`,
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
const selectedId = await p.select({
|
|
439
|
+
message: 'Select session to resume:',
|
|
440
|
+
options,
|
|
441
|
+
});
|
|
442
|
+
promptMgr.resume();
|
|
443
|
+
if (p.isCancel(selectedId)) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
selectedSession = config.loadSession(selectedId);
|
|
447
|
+
}
|
|
448
|
+
if (selectedSession && selectedSession.messages && selectedSession.messages.length > 0) {
|
|
449
|
+
harness.setHistory(selectedSession.messages);
|
|
450
|
+
if (selectedSession.model) {
|
|
451
|
+
activeModel = selectedSession.model;
|
|
286
452
|
const found = freeModels.find((m) => m.id === activeModel);
|
|
287
453
|
harness.setModel(activeModel, found?.contextLength);
|
|
288
454
|
}
|
|
289
455
|
const stats = harness.getContextStats();
|
|
290
|
-
console.log(`\n ${chalk.green('✔')} Restored
|
|
456
|
+
console.log(`\n ${chalk.green('✔')} Restored session: "${chalk.white(selectedSession.title || 'Session')}" (${chalk.green(selectedSession.messages.length)} messages, model: ${chalk.cyan(selectedSession.model)}).`);
|
|
291
457
|
console.log(` ${chalk.cyan('Context Status:')} ${stats.progressBar} ${chalk.green(`${stats.percentage}% used`)}\n`);
|
|
292
458
|
}
|
|
293
|
-
else {
|
|
294
|
-
console.log(`\n ${chalk.yellow('ℹ')} No previous session found to resume.\n`);
|
|
295
|
-
}
|
|
296
459
|
continue;
|
|
297
460
|
}
|
|
298
461
|
if (prompt === '/taste') {
|
|
@@ -315,19 +478,34 @@ async function main() {
|
|
|
315
478
|
if (prompt === '/key') {
|
|
316
479
|
promptMgr.pause();
|
|
317
480
|
const newKey = await p.password({
|
|
318
|
-
message: 'Enter
|
|
481
|
+
message: 'Enter your OpenRouter API Key:',
|
|
319
482
|
});
|
|
320
483
|
promptMgr.resume();
|
|
321
484
|
if (!p.isCancel(newKey) && newKey.trim()) {
|
|
322
485
|
apiKey = newKey.trim();
|
|
323
486
|
config.setApiKey(apiKey);
|
|
324
487
|
openRouter = new OpenRouterService(apiKey, config);
|
|
488
|
+
harness.setClient(openRouter.getClient());
|
|
325
489
|
console.log(`\n ${chalk.green('✔')} API key updated.\n`);
|
|
490
|
+
const refSpinner = ora('Fetching free models...').start();
|
|
326
491
|
try {
|
|
327
492
|
const res = await openRouter.getOrFetchFreeModels(true);
|
|
328
493
|
freeModels = res.models;
|
|
494
|
+
if (freeModels.length > 0) {
|
|
495
|
+
currentModelInfo = freeModels[0];
|
|
496
|
+
activeModel = currentModelInfo.id;
|
|
497
|
+
config.setDefaultModel(activeModel);
|
|
498
|
+
harness.setModel(activeModel, currentModelInfo.contextLength);
|
|
499
|
+
refSpinner.succeed(chalk.green(`Connected! Active Model: ${chalk.bold(activeModel)}`));
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
refSpinner.warn('No free models found on this account.');
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
catch (e) {
|
|
506
|
+
refSpinner.fail(`Failed to load models: ${e.message}`);
|
|
329
507
|
}
|
|
330
|
-
|
|
508
|
+
console.log();
|
|
331
509
|
}
|
|
332
510
|
continue;
|
|
333
511
|
}
|
|
@@ -336,27 +514,58 @@ async function main() {
|
|
|
336
514
|
console.log(`\n ${chalk.green('✔')} Context and conversation history cleared.\n`);
|
|
337
515
|
continue;
|
|
338
516
|
}
|
|
339
|
-
|
|
340
|
-
if (prompt.startsWith('/')) {
|
|
341
|
-
console.log(chalk.yellow(`\n Unrecognized command "${prompt}". Opening command options:\n`));
|
|
517
|
+
if (prompt === '/update') {
|
|
342
518
|
promptMgr.pause();
|
|
343
|
-
|
|
344
|
-
const chosenCmd = await handleSlashCommandMenu(stats);
|
|
519
|
+
await triggerManualUpdate(version, '@dastageer_44/eco-code');
|
|
345
520
|
promptMgr.resume();
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
521
|
+
console.log();
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (prompt === '/plan') {
|
|
525
|
+
const currentMode = config.getAgentMode();
|
|
526
|
+
const newMode = currentMode === 'plan' ? 'auto' : 'plan';
|
|
527
|
+
config.setAgentMode(newMode);
|
|
528
|
+
harness.updateSystemPrompt(newMode);
|
|
529
|
+
console.log(`\n ${chalk.green('✔')} Switched to ${chalk.bold(newMode.toUpperCase() + ' MODE')}.`);
|
|
530
|
+
if (newMode === 'plan') {
|
|
531
|
+
console.log(chalk.dim(' The agent will now propose changes for your approval before writing code.\n'));
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
console.log(chalk.dim(' The agent will now automatically execute all changes.\n'));
|
|
535
|
+
}
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (prompt === '/undo') {
|
|
539
|
+
promptMgr.pause();
|
|
540
|
+
const confirm = await p.confirm({
|
|
541
|
+
message: chalk.red.bold('WARNING: This will DESTROY all uncommitted code/files (git reset --hard & clean). Are you sure?'),
|
|
542
|
+
});
|
|
543
|
+
promptMgr.resume();
|
|
544
|
+
if (confirm && !p.isCancel(confirm)) {
|
|
545
|
+
const undoSpinner = ora('Discarding uncommitted changes...').start();
|
|
546
|
+
const out = await runCommand({ command: 'git reset --hard HEAD && git clean -fd' }, process.cwd());
|
|
547
|
+
undoSpinner.succeed(chalk.white('Uncommitted changes discarded.'));
|
|
548
|
+
console.log(`\n${chalk.gray(out)}\n`);
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
console.log(`\n ${chalk.gray('ℹ')} Undo cancelled.\n`);
|
|
552
|
+
}
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (prompt === '/rewind') {
|
|
556
|
+
promptMgr.pause();
|
|
557
|
+
const confirm = await p.confirm({
|
|
558
|
+
message: chalk.red.bold('WARNING: This will delete the last Git commit and discard those changes (git reset HEAD~1 --hard). Are you sure?'),
|
|
559
|
+
});
|
|
560
|
+
promptMgr.resume();
|
|
561
|
+
if (confirm && !p.isCancel(confirm)) {
|
|
562
|
+
const rewindSpinner = ora('Rewinding 1 commit...').start();
|
|
563
|
+
const out = await runCommand({ command: 'git reset HEAD~1 --hard' }, process.cwd());
|
|
564
|
+
rewindSpinner.succeed(chalk.white('Rewound 1 commit.'));
|
|
565
|
+
console.log(`\n${chalk.gray(out)}\n`);
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
console.log(`\n ${chalk.gray('ℹ')} Rewind cancelled.\n`);
|
|
360
569
|
}
|
|
361
570
|
continue;
|
|
362
571
|
}
|
|
@@ -371,7 +580,18 @@ async function main() {
|
|
|
371
580
|
}
|
|
372
581
|
continue;
|
|
373
582
|
}
|
|
583
|
+
// Ensure API Key exists before calling model
|
|
584
|
+
if (!apiKey) {
|
|
585
|
+
console.log(`\n${chalk.red.bold(' ❌ OpenRouter API Key Required')}`);
|
|
586
|
+
console.log(` To chat and write code with free models, configure your key:\n`);
|
|
587
|
+
console.log(` ${chalk.cyan.bold('Windows (PowerShell):')} ${chalk.white('$env:OPENROUTER_API_KEY="sk-or-v1-..."')}`);
|
|
588
|
+
console.log(` ${chalk.cyan.bold('Linux / macOS (Bash/Zsh):')} ${chalk.white('export OPENROUTER_API_KEY="sk-or-v1-..."')}`);
|
|
589
|
+
console.log(` ${chalk.cyan.bold('Or type:')} ${chalk.white('/key')}\n`);
|
|
590
|
+
console.log(` ${chalk.dim('Get a free key at:')} ${chalk.underline('https://openrouter.ai/keys')}\n`);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
374
593
|
// --- Run Agent Turn with Tool Calling ---
|
|
594
|
+
console.log(); // Force a blank line to scroll terminal down immediately
|
|
375
595
|
const turnSpinner = ora({ text: `${getRandomVerb()}...`, color: 'cyan' }).start();
|
|
376
596
|
// Optionally cycle the verb every 3 seconds if not running a tool
|
|
377
597
|
let isToolRunning = false;
|
|
@@ -394,11 +614,63 @@ async function main() {
|
|
|
394
614
|
});
|
|
395
615
|
clearInterval(verbInterval);
|
|
396
616
|
turnSpinner.stop();
|
|
397
|
-
console.log(`\n${result}\n`);
|
|
617
|
+
console.log(`\n${renderMarkdown(result)}\n`);
|
|
618
|
+
const history = harness.getHistory();
|
|
619
|
+
const submittedPlan = history.some(msg => msg.role === 'assistant' &&
|
|
620
|
+
msg.tool_calls?.some((tc) => tc.type === 'function' && tc.function.name === 'submit_plan'));
|
|
621
|
+
if (config.getAgentMode() === 'plan' && submittedPlan) {
|
|
622
|
+
promptMgr.pause();
|
|
623
|
+
const planChoice = await p.select({
|
|
624
|
+
message: 'How would you like to proceed?',
|
|
625
|
+
options: [
|
|
626
|
+
{ value: 'yes_once', label: '1. Execute Plan (This Time)' },
|
|
627
|
+
{ value: 'yes_always', label: '2. Execute Plan (Always Auto)' },
|
|
628
|
+
{ value: 'no', label: '3. Cancel & Reply (I have feedback / answers)' },
|
|
629
|
+
],
|
|
630
|
+
});
|
|
631
|
+
promptMgr.resume();
|
|
632
|
+
if (planChoice === 'no' || p.isCancel(planChoice)) {
|
|
633
|
+
console.log(chalk.yellow(' ℹ Plan paused. You can type your reply or feedback below.\n'));
|
|
634
|
+
}
|
|
635
|
+
if (planChoice === 'yes_always') {
|
|
636
|
+
config.setAgentMode('auto');
|
|
637
|
+
harness.updateSystemPrompt('auto');
|
|
638
|
+
}
|
|
639
|
+
if (planChoice === 'yes_once' || planChoice === 'yes_always') {
|
|
640
|
+
console.log(chalk.cyan('\nExecuting plan...\n'));
|
|
641
|
+
const execSpinner = ora({ text: `${getRandomVerb()}...`, color: 'cyan' }).start();
|
|
642
|
+
let isExecToolRunning = false;
|
|
643
|
+
const execInterval = setInterval(() => {
|
|
644
|
+
if (!isExecToolRunning)
|
|
645
|
+
execSpinner.text = `${getRandomVerb()}...`;
|
|
646
|
+
}, 3000);
|
|
647
|
+
try {
|
|
648
|
+
const execResult = await harness.runTurn('User approved the plan. Proceed with execution using write tools.', {
|
|
649
|
+
onToolStart: (toolName, args) => {
|
|
650
|
+
isExecToolRunning = true;
|
|
651
|
+
const detail = args.path || args.command || args.query || '';
|
|
652
|
+
execSpinner.text = `Running ${chalk.yellow(toolName)} ${chalk.dim(detail ? '(' + detail + ')' : '')}...`;
|
|
653
|
+
},
|
|
654
|
+
onToolEnd: (toolName) => {
|
|
655
|
+
isExecToolRunning = false;
|
|
656
|
+
execSpinner.text = `Tool ${chalk.yellow(toolName)} finished. ${getRandomVerb()}...`;
|
|
657
|
+
},
|
|
658
|
+
}, 'auto'); // Pass 'auto' mode override
|
|
659
|
+
clearInterval(execInterval);
|
|
660
|
+
execSpinner.stop();
|
|
661
|
+
console.log(`\n${renderMarkdown(execResult)}\n`);
|
|
662
|
+
}
|
|
663
|
+
catch (e) {
|
|
664
|
+
clearInterval(execInterval);
|
|
665
|
+
execSpinner.fail(chalk.red('Error during execution'));
|
|
666
|
+
console.error(chalk.red(`\n${e.message}\n`));
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
398
670
|
// Print context usage reminder if usage is moderate/high
|
|
399
671
|
const stats = harness.getContextStats();
|
|
400
672
|
printContextUsageHint(stats);
|
|
401
|
-
console.log(chalk.dim(` Total Session Tokens: ${stats.sessionTotalTokens.toLocaleString()}`));
|
|
673
|
+
console.log(chalk.dim(` Total Session Tokens: ${chalk.bold(stats.sessionTotalTokens.toLocaleString())}`));
|
|
402
674
|
}
|
|
403
675
|
catch (err) {
|
|
404
676
|
clearInterval(verbInterval);
|