ispbills-icli 5.3.0 → 6.1.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 CHANGED
@@ -47,21 +47,31 @@ icli
47
47
  ```
48
48
  $ icli
49
49
 
50
- iCli IspBills AI terminal
51
- Connected to: https://app.myisp.com
50
+ ██╗ ██████╗ ██████╗ ██████╗ ██╗██╗ ██████╗ ████████╗
51
+ ██║██╔════╝██╔═══██╗██╔══██╗██║██║ ██╔═══██╗╚══██╔══╝
52
+ ██║██║ ██║ ██║██████╔╝██║██║ ██║ ██║ ██║
53
+ ██║╚██████╗╚██████╔╝██║ ██║███████╗╚██████╔╝ ██║
54
+ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝
55
+ iCopilot — network engineer in your terminal
52
56
 
53
- Type your question. Press Ctrl+C or type "exit" to quit.
57
+ model IspBills AI · user Admin · role group_admin
58
+ url https://app.myisp.com
54
59
 
55
- iCli is vlan 82 free on all devices?
60
+ Type a message to start. /help for commands · Ctrl+C to exit
56
61
 
57
- 🧭 Assigning 3 sub-agents…
58
- › ✅ [1/3] nas#1: FREE
59
- › ✅ [2/3] nas#2: FREE
60
- › ✅ [3/3] olt#1: FREE
62
+ is vlan 82 free on all devices?
61
63
 
62
- VLAN 82 is free across all 3 reachable devices in your fleet.
64
+ working
65
+ ◈ Assigning 3 sub-agents…
66
+ ✓ [1/3] nas#1: FREE
67
+ ✓ [2/3] nas#2: FREE
68
+ ✓ [3/3] olt#1: FREE
63
69
 
64
- iCli exit
70
+ VLAN 82 is free across all 3 reachable devices in your fleet.
71
+
72
+ IspBills AI · Admin · group_admin
73
+
74
+ ❯ /exit
65
75
  ```
66
76
 
67
77
  ### Single-shot query
@@ -70,6 +80,7 @@ iCli › exit
70
80
  icli "check uptime on all routers"
71
81
  icli "show customers with overdue invoices"
72
82
  icli "what is the RX power on olt#1?"
83
+ icli --reasoning "audit the config on olt#1" # stream reasoning inline
73
84
  ```
74
85
 
75
86
  ### Commands
@@ -79,9 +90,35 @@ icli "what is the RX power on olt#1?"
79
90
  | `icli` | Start interactive REPL |
80
91
  | `icli "question"` | Single question, then exit |
81
92
  | `icli login` | Re-authenticate (or switch instance) |
93
+ | `icli logout` | Log out and clear saved credentials |
94
+ | `icli update` | Update iCli to the latest version |
82
95
  | `icli whoami` | Show current login info |
83
96
  | `icli --help` | Show help |
84
97
 
98
+ ### Flags
99
+
100
+ | Flag | Description |
101
+ |---|---|
102
+ | `--reasoning` | Stream the model's reasoning inline |
103
+ | `--loader-style STYLE` | Thinking indicator: `spinner` (default), `gradient`, or `minimal` |
104
+
105
+ ### Slash commands & keys (REPL)
106
+
107
+ Inside the REPL, type `/` to see the menu. Shortcuts:
108
+
109
+ | Slash | Description |
110
+ |---|---|
111
+ | `/check`, `/vlan`, `/onu`, `/customer`, `/ping`, `/online` | Common NOC queries |
112
+ | `/new` | Start a fresh conversation |
113
+ | `/history` | Show conversation history |
114
+ | `/clear` | Clear the screen |
115
+ | `/update` | Update iCli |
116
+ | `/logout` | Log out |
117
+ | `/help` | Show help |
118
+ | `/exit` | Quit |
119
+
120
+ Function keys: **F1** help · **F2** history · **F3** rollback last action · **F4** bulk device sweep. **Tab** autocompletes slash commands; **↑/↓** navigate input history.
121
+
85
122
  ---
86
123
 
87
124
  ## What iCli can do
@@ -93,7 +130,10 @@ iCli connects to the same AI engine as the in-browser terminal:
93
130
  - **Customer lookup** — find customers by name, IP, MAC, username, or invoice
94
131
  - **Config audits** — review running config and get concrete suggestions
95
132
  - **Web search** — looks up vendor CLI docs when it needs to
96
- - **Streaming output** — answers stream token-by-token with a live thinking indicator
133
+ - **Streaming output** — answers stream live with a thinking indicator, reasoning, and step-by-step server progress
134
+ - **Plans & connect cards** — proposed fixes render as a reviewable plan (`apply fix` to confirm); device connections show as a connect card
135
+ - **Zero dependencies** — pure Node.js (built-ins only), so it installs instantly and runs the same on Windows, macOS, and Linux
136
+ - **Terminal-aware rendering** — full-width separators, and automatic ASCII-safe glyphs on classic Windows consoles (`cmd.exe` / legacy PowerShell) so nothing shows as broken boxes
97
137
 
98
138
  ---
99
139
 
package/bin/icli.js CHANGED
@@ -5,18 +5,58 @@ import { spawnSync } from 'child_process';
5
5
  import { readFileSync } from 'fs';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { dirname, join } from 'path';
8
- import chalk from 'chalk';
8
+
9
9
  import { loadConfig, configPath, clearConfig } from '../src/config.js';
10
10
  import { loginFlow } from '../src/auth.js';
11
- import { inner, onResize, boxTop, boxBot, boxMid, boxRow, bottomPalette } from '../src/layout.js';
11
+ import { streamChat } from '../src/client.js';
12
+ import { TuiRenderer } from '../src/renderer.js';
13
+ import { Loader } from '../src/loader.js';
14
+ import { printBanner } from '../src/banner.js';
15
+ import {
16
+ SLASH_COMMANDS, slashCompleter, slashToQuestion, printSlashMenu, printHistory,
17
+ } from '../src/commands.js';
12
18
  import {
13
- streamChat, printBanner, printSlashMenu, printUserBox,
14
- SLASH_COMMANDS, slashCompleter,
15
- } from '../src/chat.js';
19
+ RESET, BOLD, DIM, GRAY, WHITE, ACCENT, GREEN, YELLOW, RED, CYAN, rule, glyphs,
20
+ } from '../src/ui.js';
16
21
 
17
22
  const PKG_NAME = 'ispbills-icli';
18
23
 
19
- /** Read this CLI's installed version from its package.json. */
24
+ // ── Flags ────────────────────────────────────────────────────────────────────
25
+ const rawArgs = argv.slice(2);
26
+
27
+ // Flags that consume the following token as their value.
28
+ const VALUE_FLAGS = new Set(['--loader-style']);
29
+
30
+ function has(name) {
31
+ return rawArgs.includes(name);
32
+ }
33
+ function flag(name) {
34
+ const i = rawArgs.indexOf(name);
35
+ return i !== -1 && i + 1 < rawArgs.length ? rawArgs[i + 1] : undefined;
36
+ }
37
+
38
+ // Positional args = everything that is not a flag or a flag's value.
39
+ const positional = [];
40
+ for (let i = 0; i < rawArgs.length; i++) {
41
+ const a = rawArgs[i];
42
+ if (a.startsWith('-')) {
43
+ if (VALUE_FLAGS.has(a)) i++; // skip this flag's value
44
+ continue;
45
+ }
46
+ positional.push(a);
47
+ }
48
+
49
+ const cmd = positional[0] ?? '';
50
+
51
+ const display = {
52
+ reasoning: has('--reasoning'),
53
+ loader: {
54
+ text: 'Thinking',
55
+ style: flag('--loader-style') || 'spinner',
56
+ },
57
+ };
58
+
59
+ // ── Version / self-update ──────────────────────────────────────────────────────
20
60
  function pkgVersion() {
21
61
  try {
22
62
  const p = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
@@ -26,15 +66,10 @@ function pkgVersion() {
26
66
  }
27
67
  }
28
68
 
29
- /**
30
- * Self-update: install the latest published version globally via npm.
31
- * Returns true on success. The running process keeps the old code until the
32
- * next launch, so callers should tell the user to restart.
33
- */
34
- function selfUpdate(P) {
69
+ function selfUpdate() {
35
70
  const cur = pkgVersion();
36
- process.stdout.write('\n ' + P.dim('Current version: ') + chalk.white(cur) + '\n');
37
- process.stdout.write(' ' + P.muted('Updating ' + PKG_NAME + ' to the latest version…') + '\n\n');
71
+ process.stdout.write(`\n ${DIM}Current version: ${RESET}${WHITE}${cur}${RESET}\n`);
72
+ process.stdout.write(` ${GRAY}Updating ${PKG_NAME} to the latest version…${RESET}\n\n`);
38
73
 
39
74
  const npm = platform === 'win32' ? 'npm.cmd' : 'npm';
40
75
  const res = spawnSync(npm, ['install', '-g', `${PKG_NAME}@latest`], {
@@ -43,46 +78,19 @@ function selfUpdate(P) {
43
78
  });
44
79
 
45
80
  if (res.status === 0) {
46
- process.stdout.write('\n ' + P.ok('✓') + ' iCli updated. ' +
47
- P.dim('Restart iCli to use the new version.') + '\n\n');
81
+ process.stdout.write(`\n ${GREEN}${glyphs.check}${RESET} iCli updated. ${DIM}Restart iCli to use the new version.${RESET}\n\n`);
48
82
  return true;
49
83
  }
50
- process.stdout.write('\n ' + P.err('✖') + ' Update failed. Try manually:\n');
51
- process.stdout.write(' ' + P.accent(`npm install -g ${PKG_NAME}@latest`) + '\n');
52
- process.stdout.write(' ' + P.dim('On some systems this needs elevated privileges (sudo / Administrator).') + '\n\n');
84
+ process.stdout.write(`\n ${RED}${glyphs.cross}${RESET} Update failed. Try manually:\n`);
85
+ process.stdout.write(` ${ACCENT}npm install -g ${PKG_NAME}@latest${RESET}\n`);
86
+ process.stdout.write(` ${DIM}On some systems this needs elevated privileges (sudo / Administrator).${RESET}\n\n`);
53
87
  return false;
54
88
  }
55
89
 
56
- const P = {
57
- border: chalk.hex('#303134'),
58
- dim: chalk.hex('#9AA0A6'),
59
- muted: chalk.hex('#80868B'),
60
- accent: chalk.hex('#8AB4F8').bold,
61
- accentD: chalk.hex('#8AB4F8'),
62
- bold: chalk.bold.white,
63
- ok: chalk.green,
64
- warn: chalk.yellow,
65
- err: chalk.red,
66
- };
67
-
68
- const args = argv.slice(2);
69
- const cmd = args[0] ?? '';
70
-
71
- // ── Session context (shown in top bar) ─────────────────────────────────────────
72
- const ctx = {
73
- device: '',
74
- aiMode: 'MANUAL', // 'AUTOPILOT ON' | 'MANUAL'
75
- operator: '',
76
- };
77
-
78
90
  // ── Help ───────────────────────────────────────────────────────────────────────
79
91
  function printHelp() {
80
- process.stdout.write('\n');
81
- process.stdout.write(boxTop('iCli · Help', P.border) + '\n');
82
-
83
- process.stdout.write(boxRow('', P.border) + '\n');
84
- process.stdout.write(boxRow(P.bold('Usage'), P.border) + '\n');
85
- process.stdout.write(boxRow('', P.border) + '\n');
92
+ console.log(`\n ${BOLD}iCli — IspBills AI in your terminal${RESET}\n`);
93
+ console.log(` ${BOLD}Usage${RESET}`);
86
94
  [
87
95
  ['icli', 'Interactive REPL with slash commands'],
88
96
  ['icli "question"', 'Single-shot query then exit'],
@@ -91,163 +99,116 @@ function printHelp() {
91
99
  ['icli update', 'Update iCli to the latest version'],
92
100
  ['icli whoami', 'Show current session info'],
93
101
  ['icli --help', 'Show this help'],
94
- ].forEach(([c, d]) =>
95
- process.stdout.write(boxRow(P.accent(c.padEnd(20)) + P.dim(d), P.border) + '\n')
96
- );
102
+ ].forEach(([c, d]) => console.log(` ${ACCENT}${c.padEnd(20)}${RESET}${DIM}${d}${RESET}`));
103
+
104
+ console.log(`\n ${BOLD}Flags${RESET}`);
105
+ [
106
+ ['--reasoning', 'Stream the model\'s reasoning inline'],
107
+ ['--loader-style STYLE', 'spinner | gradient | minimal'],
108
+ ].forEach(([c, d]) => console.log(` ${ACCENT}${c.padEnd(24)}${RESET}${DIM}${d}${RESET}`));
97
109
 
98
- process.stdout.write(boxMid(P.border) + '\n');
99
- process.stdout.write(boxRow(P.bold('Slash commands'), P.border) + '\n');
100
- process.stdout.write(boxRow('', P.border) + '\n');
110
+ console.log(`\n ${BOLD}Slash commands${RESET}`);
101
111
  for (const { cmd: c, hint, desc } of SLASH_COMMANDS) {
102
- const key = (c + (hint ? ' '+hint : '')).padEnd(22);
103
- process.stdout.write(boxRow(P.accent(key) + P.dim(desc), P.border) + '\n');
112
+ const key = (c + (hint ? ' ' + hint : '')).padEnd(20);
113
+ console.log(` ${ACCENT}${key}${RESET}${DIM}${desc}${RESET}`);
104
114
  }
105
115
 
106
- process.stdout.write(boxMid(P.border) + '\n');
107
- process.stdout.write(boxRow(P.bold('Function key shortcuts'), P.border) + '\n');
108
- process.stdout.write(boxRow('', P.border) + '\n');
116
+ console.log(`\n ${BOLD}Function keys${RESET}`);
109
117
  [
110
- ['F1', 'Open documentation'],
111
- ['F2', 'Show conversation history'],
112
- ['F3', 'Rollback last AI action'],
113
- ['F4', 'Run bulk device sweep'],
114
- ['Tab', 'Autocomplete slash commands'],
115
- ['↑ / ↓', 'Navigate input history'],
116
- ['Ctrl+L', 'Clear screen'],
117
- ['Ctrl+C', 'Cancel / exit'],
118
- ].forEach(([k, d]) =>
119
- process.stdout.write(boxRow(P.warn(k.padEnd(14)) + P.dim(d), P.border) + '\n')
120
- );
121
- process.stdout.write(boxRow('', P.border) + '\n');
122
- process.stdout.write(boxBot(P.border) + '\n');
123
- process.stdout.write(bottomPalette() + '\n\n');
118
+ ['F1', 'Show help'],
119
+ ['F2', 'Show conversation history'],
120
+ ['F3', 'Rollback last AI action'],
121
+ ['F4', 'Run bulk device sweep'],
122
+ ['Tab', 'Autocomplete slash commands'],
123
+ ['↑ / ↓', 'Navigate input history'],
124
+ ['Ctrl+C', 'Cancel / exit'],
125
+ ].forEach(([k, d]) => console.log(` ${YELLOW}${k.padEnd(10)}${RESET}${DIM}${d}${RESET}`));
126
+ console.log();
124
127
  }
125
128
 
126
129
  // ── Auth ───────────────────────────────────────────────────────────────────────
127
130
  async function ensureAuth() {
128
131
  let cfg = loadConfig();
129
132
  if (!cfg) {
130
- process.stdout.write(P.warn('\n No saved credentials. Setting up iCli.\n\n'));
133
+ process.stdout.write(`\n ${YELLOW}No saved credentials. Setting up iCli.${RESET}\n\n`);
131
134
  cfg = await loginFlow();
132
- process.stdout.write('\n ' + P.ok('✓') + ' Logged in as ' +
133
- chalk.white(cfg.user?.name ?? cfg.user?.email) +
134
- P.dim(' · ' + (cfg.user?.role ?? 'operator')) + '\n\n');
135
+ process.stdout.write(`\n ${GREEN}${glyphs.check}${RESET} Logged in as ${WHITE}${cfg.user?.name ?? cfg.user?.email}${RESET}` +
136
+ `${DIM} · ${cfg.user?.role ?? 'operator'}${RESET}\n\n`);
135
137
  }
136
138
  return cfg;
137
139
  }
138
140
 
139
- // ── Slash question ───────────────────────────────────────────────────────────
140
- function slashToQuestion(line) {
141
- const [sc, ...rest] = line.trim().split(/\s+/);
142
- const arg = rest.join(' ');
143
- switch (sc) {
144
- case '/check': return arg ? `run diagnostics on ${arg}` : 'check status of all devices';
145
- case '/vlan': return arg ? `is vlan ${arg} in use or free?` : 'list all vlans in use';
146
- case '/onu': return arg ? `show ONU ${arg} status and signal level` : 'list all ONUs';
147
- case '/customer': return arg ? `look up customer ${arg}` : 'show recent customers';
148
- case '/ping': return arg ? `ping ${arg} through the network` : 'which devices are reachable?';
149
- case '/online': return 'list currently online PPPoE sessions';
150
- default: return null;
141
+ // ── One conversation turn ───────────────────────────────────────────────────────
142
+ async function runTurn(cfg, history, renderer, loader, question, { echo = false } = {}) {
143
+ if (echo) console.log(`\n ${GRAY}${glyphs.prompt}${RESET} ${DIM}${question}${RESET}`);
144
+ history.push({ role: 'user', content: question });
145
+
146
+ const abort = new AbortController();
147
+ const onSigint = () => abort.abort();
148
+ process.once('SIGINT', onSigint);
149
+
150
+ console.log();
151
+ loader.start();
152
+ let stopped = false;
153
+ const stopOnce = () => { if (!stopped) { loader.stop(); stopped = true; } };
154
+
155
+ try {
156
+ const { reply, text, meta } = await streamChat(cfg, [...history], {
157
+ signal: abort.signal,
158
+ onEvent: (ev) => { stopOnce(); renderer.handle(ev); },
159
+ });
160
+ stopOnce();
161
+ if (meta.model) cfg._model = meta.model;
162
+ renderer.renderReply(reply, meta);
163
+ if (text) history.push({ role: 'assistant', content: text });
164
+ console.log();
165
+ } catch (e) {
166
+ stopOnce();
167
+ renderer.endStreaming();
168
+ if (abort.signal.aborted) {
169
+ console.log(`\n ${DIM}Interrupted.${RESET}\n`);
170
+ } else {
171
+ console.log(`\n ${RED}${glyphs.cross}${RESET} ${WHITE}${e.message}${RESET}\n`);
172
+ }
173
+ } finally {
174
+ process.removeListener('SIGINT', onSigint);
151
175
  }
152
176
  }
153
177
 
154
178
  // ── Single-shot ────────────────────────────────────────────────────────────────
155
179
  async function singleShot(cfg, question) {
156
- printUserBox(question);
157
- try {
158
- await streamChat(cfg, [{ role: 'user', content: question }]);
159
- } catch (e) {
160
- process.stdout.write('\n ' + P.err('✖ ') + chalk.white(e.message) + '\n\n');
161
- exit(1);
162
- }
180
+ const renderer = new TuiRenderer({ display });
181
+ const loader = new Loader(display.loader);
182
+ await runTurn(cfg, [], renderer, loader, question, { echo: true });
163
183
  }
164
184
 
165
- // ── REPL ───────────────────────────────────────────────────────────────────────
185
+ // ── Interactive REPL ─────────────────────────────────────────────────────────────
166
186
  async function interactiveRepl(cfg) {
167
187
  const history = [];
168
- const PROMPT = () => ' ' + P.accentD('❯') + ' ';
188
+ const renderer = new TuiRenderer({ display });
189
+ const loader = new Loader(display.loader);
190
+ const PROMPT = () => ` ${ACCENT}${glyphs.prompt}${RESET} `;
191
+ let busy = false;
169
192
 
170
- let isProcessing = false;
171
-
172
- // Populate operator from cfg
173
- if (cfg.user?.name) ctx.operator = cfg.user.name;
174
- if (cfg.user?.role) ctx.aiMode = cfg.user.role === 'noc' ? 'AUTOPILOT ON' : 'MANUAL';
175
-
176
- printBanner(cfg, {}, ctx);
193
+ printBanner(cfg, { model: cfg._model });
177
194
 
178
195
  const rl = createInterface({
179
- input, output,
180
- prompt: PROMPT(),
181
- completer: slashCompleter,
182
- terminal: true,
196
+ input, output, prompt: PROMPT(), completer: slashCompleter, terminal: true,
183
197
  });
184
198
 
185
- onResize(() => rl.setPrompt(PROMPT()));
186
-
187
- // ── F-key handling via keypress events ───────────────────────────────────────
199
+ // F-key shortcuts
188
200
  emitKeypressEvents(process.stdin);
189
-
190
201
  process.stdin.on('keypress', async (_ch, key) => {
191
- if (!key || rl.closed || isProcessing) return;
192
-
193
- if (key.name === 'f1') {
194
- process.stdout.write('\n');
195
- printHelp();
196
- rl.prompt();
197
- return;
198
- }
199
-
200
- if (key.name === 'f2') {
201
- // Show history
202
- const turns = history.filter(m => m.role === 'user');
203
- process.stdout.write('\n');
204
- if (!turns.length) {
205
- process.stdout.write(P.muted(' No history yet.\n\n'));
206
- } else {
207
- process.stdout.write(boxTop('History', P.border) + '\n');
208
- turns.forEach((m, i) =>
209
- process.stdout.write(
210
- boxRow(P.dim(String(i + 1).padStart(2) + '.') + ' ' +
211
- chalk.white(m.content.replace(/\s+/g, ' ').slice(0, inner() - 5)), P.border) + '\n')
212
- );
213
- process.stdout.write(boxBot(P.border) + '\n\n');
214
- }
215
- rl.prompt();
216
- return;
217
- }
218
-
219
- if (key.name === 'f3') {
220
- rl.pause();
221
- const q = 'rollback the last change you made';
222
- process.stdout.write('\n ' + P.muted('→ ') + P.dim(q) + '\n');
223
- printUserBox(q);
224
- history.push({ role: 'user', content: q });
225
- try {
226
- const answer = await streamChat(cfg, [...history]);
227
- if (answer) history.push({ role: 'assistant', content: answer });
228
- } catch (e) {
229
- process.stdout.write('\n ' + P.err('✖ ') + chalk.white(e.message) + '\n\n');
230
- }
231
- rl.resume();
232
- rl.prompt();
233
- return;
234
- }
235
-
236
- if (key.name === 'f4') {
237
- rl.pause();
238
- const q = 'run a bulk sweep of all devices and report status';
239
- process.stdout.write('\n ' + P.muted('→ ') + P.dim(q) + '\n');
240
- printUserBox(q);
241
- history.push({ role: 'user', content: q });
242
- try {
243
- const answer = await streamChat(cfg, [...history]);
244
- if (answer) history.push({ role: 'assistant', content: answer });
245
- } catch (e) {
246
- process.stdout.write('\n ' + P.err('✖ ') + chalk.white(e.message) + '\n\n');
247
- }
248
- rl.resume();
249
- rl.prompt();
250
- return;
202
+ if (!key || rl.closed || busy) return;
203
+ if (key.name === 'f1') { printHelp(); rl.prompt(); return; }
204
+ if (key.name === 'f2') { printHistory(history); rl.prompt(); return; }
205
+ if (key.name === 'f3' || key.name === 'f4') {
206
+ const q = key.name === 'f3'
207
+ ? 'rollback the last change you made'
208
+ : 'run a bulk sweep of all devices and report status';
209
+ busy = true; rl.pause();
210
+ await runTurn(cfg, history, renderer, loader, q, { echo: true });
211
+ busy = false; rl.resume(); rl.prompt();
251
212
  }
252
213
  });
253
214
 
@@ -257,130 +218,93 @@ async function interactiveRepl(cfg) {
257
218
  const q = line.trim();
258
219
  if (!q) { rl.prompt(); return; }
259
220
 
221
+ // Locally-handled commands
260
222
  if (q === '/exit' || q === 'exit' || q === 'quit') {
261
- process.stdout.write(P.muted('\n Goodbye.\n\n')); rl.close(); exit(0);
223
+ console.log(`\n ${DIM}Goodbye.${RESET}\n`); rl.close(); return;
262
224
  }
263
225
  if (q === '/clear' || q === 'clear') {
264
- process.stdout.write('\x1Bc');
265
- printBanner(cfg, {}, ctx);
266
- rl.prompt();
267
- return;
226
+ process.stdout.write('\x1Bc'); printBanner(cfg, { model: cfg._model }); rl.prompt(); return;
268
227
  }
228
+ if (q === '/new') {
229
+ history.length = 0;
230
+ console.log(`\n ${GREEN}${glyphs.check}${RESET} ${DIM}New conversation started.${RESET}\n`); rl.prompt(); return;
231
+ }
232
+ if (q === '/help' || q === 'help') { printHelp(); rl.prompt(); return; }
233
+ if (q === '/history' || q === 'history') { printHistory(history); rl.prompt(); return; }
234
+ if (q === '/') { printSlashMenu(); rl.prompt(); return; }
269
235
  if (q === '/update' || q === 'update') {
270
- rl.pause();
271
- selfUpdate(P);
272
- rl.resume();
273
- rl.prompt();
274
- return;
236
+ rl.pause(); selfUpdate(); rl.resume(); rl.prompt(); return;
275
237
  }
276
238
  if (q === '/logout' || q === 'logout') {
277
239
  const cleared = clearConfig();
278
- process.stdout.write('\n ' + (cleared ? P.ok('✓') + ' Logged out — credentials cleared.'
279
- : P.warn('Already logged out.')) +
280
- '\n ' + P.dim('Run ') + P.accent('icli login') + P.dim(' to sign in again.') + '\n\n');
281
- rl.close();
282
- exit(0);
283
- }
284
- if (q === '/help' || q === 'help') { printHelp(); rl.prompt(); return; }
285
- if (q === '/history' || q === 'history') {
286
- const turns = history.filter(m => m.role === 'user');
287
- if (!turns.length) {
288
- process.stdout.write(P.muted('\n No history yet.\n\n'));
289
- } else {
290
- process.stdout.write('\n' + boxTop('History', P.border) + '\n');
291
- turns.forEach((m, i) =>
292
- process.stdout.write(boxRow(P.dim(String(i+1).padStart(2)+'.') + ' ' +
293
- chalk.white(m.content.slice(0, inner() - 5)), P.border) + '\n')
294
- );
295
- process.stdout.write(boxBot(P.border) + '\n\n');
296
- }
297
- rl.prompt(); return;
240
+ console.log(`\n ${cleared ? GREEN + glyphs.check + RESET + ' Logged out — credentials cleared.' : YELLOW + 'Already logged out.' + RESET}`);
241
+ console.log(` ${DIM}Run ${RESET}${ACCENT}icli login${RESET}${DIM} to sign in again.${RESET}\n`);
242
+ rl.close(); return;
298
243
  }
299
- if (q === '/') { printSlashMenu(); rl.prompt(); return; }
300
244
 
245
+ // AI shortcut → question, else pass through verbatim
301
246
  let question = q;
302
247
  if (q.startsWith('/')) {
303
248
  const resolved = slashToQuestion(q);
304
- if (resolved) {
305
- question = resolved;
306
- process.stdout.write(' ' + P.muted('→ ') + P.dim(question) + '\n');
307
- }
308
- }
309
-
310
- rl.pause();
311
- history.push({ role: 'user', content: question });
312
- printUserBox(question);
313
-
314
- try {
315
- const answer = await streamChat(cfg, [...history]);
316
- if (answer) history.push({ role: 'assistant', content: answer });
317
- } catch (e) {
318
- process.stdout.write('\n ' + P.err('✖ ') + chalk.white(e.message) + '\n\n');
249
+ if (resolved) question = resolved;
250
+ else { console.log(`\n ${DIM}Unknown command: ${q}. Type ${RESET}${ACCENT}/help${RESET}${DIM}.${RESET}\n`); rl.prompt(); return; }
319
251
  }
320
252
 
321
- rl.resume();
322
- rl.prompt();
253
+ busy = true; rl.pause();
254
+ await runTurn(cfg, history, renderer, loader, question, { echo: q.startsWith('/') });
255
+ busy = false; rl.resume(); rl.prompt();
323
256
  });
324
257
 
325
- rl.on('close', () => { process.stdout.write(P.muted('\n Goodbye.\n\n')); exit(0); });
258
+ rl.on('close', () => { console.log(`\n ${DIM}Goodbye.${RESET}\n`); exit(0); });
326
259
  }
327
260
 
328
- // ── Main ───────────────────────────────────────────────────────────────────────
261
+ // ── Main ─────────────────────────────────────────────────────────────────────────
329
262
  async function main() {
330
263
  if (cmd === '--help' || cmd === '-h' || cmd === 'help') { printHelp(); return; }
331
264
 
332
265
  if (cmd === 'login') {
333
266
  const cfg = await loginFlow();
334
- process.stdout.write('\n ' + P.ok('✓') + ' Logged in as ' +
335
- chalk.white(cfg.user?.name ?? cfg.user?.email) +
336
- P.dim(' · ' + (cfg.user?.role ?? 'operator')) + '\n');
337
- process.stdout.write(' ' + P.dim('Config: ' + configPath()) + '\n\n');
267
+ console.log(`\n ${GREEN}${glyphs.check}${RESET} Logged in as ${WHITE}${cfg.user?.name ?? cfg.user?.email}${RESET}${DIM} · ${cfg.user?.role ?? 'operator'}${RESET}`);
268
+ console.log(` ${DIM}Config: ${configPath()}${RESET}\n`);
338
269
  return;
339
270
  }
340
271
 
341
- if (cmd === 'update') {
342
- const ok = selfUpdate(P);
343
- exit(ok ? 0 : 1);
344
- }
272
+ if (cmd === 'update') { exit(selfUpdate() ? 0 : 1); }
345
273
 
346
274
  if (cmd === 'logout') {
347
275
  const cleared = clearConfig();
348
276
  if (cleared) {
349
- process.stdout.write('\n ' + P.ok('✓') + ' Logged out — credentials cleared.\n');
350
- process.stdout.write(' ' + P.dim('Run ') + P.accent('icli login') + P.dim(' to sign in again.') + '\n\n');
277
+ console.log(`\n ${GREEN}${glyphs.check}${RESET} Logged out — credentials cleared.`);
278
+ console.log(` ${DIM}Run ${RESET}${ACCENT}icli login${RESET}${DIM} to sign in again.${RESET}\n`);
351
279
  } else {
352
- process.stdout.write('\n ' + P.warn('Not logged in.') + ' Nothing to clear.\n\n');
280
+ console.log(`\n ${YELLOW}Not logged in.${RESET} Nothing to clear.\n`);
353
281
  }
354
282
  return;
355
283
  }
356
284
 
357
285
  if (cmd === 'whoami') {
358
286
  const cfg = loadConfig();
359
- if (!cfg) {
360
- process.stdout.write('\n ' + P.warn('Not logged in.') + ' Run ' + P.accent('icli login') + '\n\n');
361
- return;
362
- }
363
- process.stdout.write('\n' + boxTop('Session', P.border) + '\n');
287
+ if (!cfg) { console.log(`\n ${YELLOW}Not logged in.${RESET} Run ${ACCENT}icli login${RESET}\n`); return; }
288
+ console.log(`\n ${BOLD}Session${RESET}`);
364
289
  [['url', cfg.url], ['name', cfg.user?.name], ['email', cfg.user?.email], ['role', cfg.user?.role]]
365
290
  .filter(([, v]) => v)
366
- .forEach(([k, v]) =>
367
- process.stdout.write(boxRow(P.dim(k.padEnd(7)) + chalk.white(v), P.border) + '\n')
368
- );
369
- process.stdout.write(boxBot(P.border) + '\n\n');
291
+ .forEach(([k, v]) => console.log(` ${DIM}${k.padEnd(7)}${RESET}${WHITE}${v}${RESET}`));
292
+ console.log();
370
293
  return;
371
294
  }
372
295
 
373
296
  const cfg = await ensureAuth();
374
297
 
375
- if (cmd && !cmd.startsWith('-')) {
376
- await singleShot(cfg, args.join(' '));
298
+ // Single-shot: any positional (non-flag) args form the question.
299
+ if (positional.length) {
300
+ await singleShot(cfg, positional.join(' '));
377
301
  return;
378
302
  }
379
303
 
380
304
  await interactiveRepl(cfg);
381
305
  }
382
306
 
383
- main().catch(e => {
384
- process.stdout.write('\n ' + P.err('✖ ') + chalk.white(e.message) + '\n\n');
307
+ main().catch((e) => {
308
+ console.log(`\n ${RED}${glyphs.cross}${RESET} ${WHITE}${e.message}${RESET}\n`);
385
309
  exit(1);
386
310
  });