@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/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.0.0';
35
+ return pkg.version || '1.2.0';
34
36
  }
35
37
  }
36
38
  catch { }
37
- return '1.0.0';
39
+ return '1.2.0';
38
40
  }
39
41
  const config = new ConfigManager();
40
42
  /**
41
- * Robust Readline Prompt Manager that stays alive on Windows/macOS/Linux
42
- * and catches instant '/' keypress without requiring Enter.
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
- rl;
48
+ sigintCount = 0;
49
+ isPaused = false;
46
50
  constructor() {
47
- this.rl = readline.createInterface({
48
- input: process.stdin,
49
- output: process.stdout,
50
- terminal: true,
51
- });
51
+ readline.emitKeypressEvents(process.stdin);
52
52
  }
53
- ask(promptStr) {
53
+ ask(promptPrefix, getStats) {
54
54
  return new Promise((resolve) => {
55
- this.rl.resume();
56
- const onData = (chunk) => {
57
- const str = chunk.toString();
58
- // If line is empty and user pressed '/', trigger instant slash menu
59
- if (this.rl.line.trim() === '' && str === '/') {
60
- process.stdin.removeListener('data', onData);
61
- readline.clearLine(process.stdout, 0);
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
- this.rl.pause();
64
- resolve('/');
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
- process.stdin.on('data', onData);
68
- this.rl.question(promptStr, (answer) => {
69
- process.stdin.removeListener('data', onData);
70
- resolve(answer);
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.rl.pause();
253
+ this.isPaused = true;
254
+ if (process.stdin.setRawMode)
255
+ process.stdin.setRawMode(false);
76
256
  }
77
257
  resume() {
78
- this.rl.resume();
79
- }
80
- close() {
81
- this.rl.close();
258
+ this.isPaused = false;
82
259
  }
260
+ close() { }
83
261
  }
84
- async function promptOnboardingApiKey() {
85
- p.intro(chalk.bgGreen.black(' eco-code Setup '));
86
- const method = await p.select({
87
- message: 'How would you like to configure your OpenRouter API Key?',
88
- options: [
89
- { value: 'interactive', label: '1. Paste API Key here in terminal (Recommended)' },
90
- { value: 'env', label: '2. Show how to set OPENROUTER_API_KEY environment variable' },
91
- { value: 'config', label: `3. Show config file path (~/.eco-code/config.json)` },
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
- if (!apiKey) {
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
- try {
185
- const result = await openRouter.getOrFetchFreeModels(false);
186
- freeModels = result.models;
187
- if (freeModels.length === 0) {
188
- spinner.fail('No free models found on OpenRouter.');
189
- console.log(chalk.yellow('\nPlease check your API key or visit https://openrouter.ai/models?max_price=0'));
190
- process.exit(1);
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
- let currentModelInfo = freeModels.find((m) => m.id === config.getDefaultModel()) || freeModels[0];
201
- let activeModel = currentModelInfo.id;
202
- config.setDefaultModel(activeModel);
203
- console.log(` ${chalk.dim('Active Model:')} ${chalk.green.bold(activeModel)} ${chalk.dim(`(${currentModelInfo.contextLength.toLocaleString()} tokens)`)}`);
204
- console.log(` ${chalk.dim('Press / for commands menu, /exit to quit.')}\n`);
205
- const harness = new AgentHarness(openRouter.getClient(), activeModel, currentModelInfo.contextLength, config, process.cwd());
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 promptStr = `${chalk.green.bold('eco-code')} ${chalk.cyan('❯')} `;
210
- const rawInput = await promptMgr.ask(promptStr);
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 session = config.loadLatestSession();
282
- if (session && session.messages && session.messages.length > 1) {
283
- harness.setHistory(session.messages);
284
- if (session.model) {
285
- activeModel = session.model;
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 previous session (${chalk.green(session.messages.length)} messages, model: ${chalk.cyan(session.model)}).`);
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 new OpenRouter API Key:',
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
- catch { }
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
- // Safeguard for typos on slash commands (e.g. /mode;)
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
- const stats = harness.getContextStats();
344
- const chosenCmd = await handleSlashCommandMenu(stats);
519
+ await triggerManualUpdate(version, '@dastageer_44/eco-code');
345
520
  promptMgr.resume();
346
- if (chosenCmd) {
347
- if (chosenCmd === '/compact') {
348
- const { before, after } = harness.compactContext();
349
- const newStats = harness.getContextStats();
350
- console.log(`\n ${chalk.green('✔')} Compacted context from ${chalk.yellow(before)} to ${chalk.green(after)} messages (${newStats.percentage}% used).\n`);
351
- }
352
- else if (chosenCmd === '/help') {
353
- printHelp();
354
- }
355
- else if (chosenCmd === '/exit') {
356
- console.log('\nHappy coding! 👋\n');
357
- promptMgr.close();
358
- process.exit(0);
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);