ispbills-icli 5.3.0 → 6.0.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 +50 -11
- package/bin/icli.js +170 -246
- package/package.json +2 -7
- package/src/banner.js +42 -0
- package/src/client.js +100 -0
- package/src/commands.js +67 -0
- package/src/loader.js +77 -0
- package/src/renderer.js +162 -0
- package/src/session.js +41 -0
- package/src/ui.js +61 -0
- package/src/chat.js +0 -379
- package/src/layout.js +0 -209
package/README.md
CHANGED
|
@@ -47,21 +47,31 @@ icli
|
|
|
47
47
|
```
|
|
48
48
|
$ icli
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
██╗ ██████╗ ██████╗ ██████╗ ██╗██╗ ██████╗ ████████╗
|
|
51
|
+
██║██╔════╝██╔═══██╗██╔══██╗██║██║ ██╔═══██╗╚══██╔══╝
|
|
52
|
+
██║██║ ██║ ██║██████╔╝██║██║ ██║ ██║ ██║
|
|
53
|
+
██║╚██████╗╚██████╔╝██║ ██║███████╗╚██████╔╝ ██║
|
|
54
|
+
╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝
|
|
55
|
+
iCopilot — network engineer in your terminal
|
|
52
56
|
|
|
53
|
-
|
|
57
|
+
model IspBills AI · user Admin · role group_admin
|
|
58
|
+
url https://app.myisp.com
|
|
54
59
|
|
|
55
|
-
|
|
60
|
+
Type a message to start. /help for commands · Ctrl+C to exit
|
|
56
61
|
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,9 @@ 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
|
|
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
|
|
97
136
|
|
|
98
137
|
---
|
|
99
138
|
|
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
|
-
|
|
8
|
+
|
|
9
9
|
import { loadConfig, configPath, clearConfig } from '../src/config.js';
|
|
10
10
|
import { loginFlow } from '../src/auth.js';
|
|
11
|
-
import {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
} from '../src/chat.js';
|
|
19
|
+
RESET, BOLD, DIM, GRAY, WHITE, ACCENT, GREEN, YELLOW, RED, CYAN, rule,
|
|
20
|
+
} from '../src/ui.js';
|
|
16
21
|
|
|
17
22
|
const PKG_NAME = 'ispbills-icli';
|
|
18
23
|
|
|
19
|
-
|
|
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(
|
|
37
|
-
process.stdout.write(
|
|
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(
|
|
47
|
-
P.dim('Restart iCli to use the new version.') + '\n\n');
|
|
81
|
+
process.stdout.write(`\n ${GREEN}✓${RESET} iCli updated. ${DIM}Restart iCli to use the new version.${RESET}\n\n`);
|
|
48
82
|
return true;
|
|
49
83
|
}
|
|
50
|
-
process.stdout.write(
|
|
51
|
-
process.stdout.write(
|
|
52
|
-
process.stdout.write(
|
|
84
|
+
process.stdout.write(`\n ${RED}✖${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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
103
|
-
|
|
112
|
+
const key = (c + (hint ? ' ' + hint : '')).padEnd(20);
|
|
113
|
+
console.log(` ${ACCENT}${key}${RESET}${DIM}${desc}${RESET}`);
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
|
|
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',
|
|
111
|
-
['F2',
|
|
112
|
-
['F3',
|
|
113
|
-
['F4',
|
|
114
|
-
['Tab',
|
|
115
|
-
['↑ / ↓',
|
|
116
|
-
['Ctrl+
|
|
117
|
-
|
|
118
|
-
|
|
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(
|
|
133
|
+
process.stdout.write(`\n ${YELLOW}No saved credentials. Setting up iCli.${RESET}\n\n`);
|
|
131
134
|
cfg = await loginFlow();
|
|
132
|
-
process.stdout.write(
|
|
133
|
-
|
|
134
|
-
P.dim(' · ' + (cfg.user?.role ?? 'operator')) + '\n\n');
|
|
135
|
+
process.stdout.write(`\n ${GREEN}✓${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
|
-
// ──
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
141
|
+
// ── One conversation turn ───────────────────────────────────────────────────────
|
|
142
|
+
async function runTurn(cfg, history, renderer, loader, question, { echo = false } = {}) {
|
|
143
|
+
if (echo) console.log(`\n ${GRAY}❯${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}✖${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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
188
|
+
const renderer = new TuiRenderer({ display });
|
|
189
|
+
const loader = new Loader(display.loader);
|
|
190
|
+
const PROMPT = () => ` ${ACCENT}❯${RESET} `;
|
|
191
|
+
let busy = false;
|
|
169
192
|
|
|
170
|
-
|
|
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
|
-
|
|
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 ||
|
|
192
|
-
|
|
193
|
-
if (key.name === '
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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}✓${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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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 + '✓' + 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
|
-
|
|
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.
|
|
322
|
-
|
|
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', () => {
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
P.dim(' · ' + (cfg.user?.role ?? 'operator')) + '\n');
|
|
337
|
-
process.stdout.write(' ' + P.dim('Config: ' + configPath()) + '\n\n');
|
|
267
|
+
console.log(`\n ${GREEN}✓${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
|
-
|
|
350
|
-
|
|
277
|
+
console.log(`\n ${GREEN}✓${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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
376
|
-
|
|
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
|
-
|
|
307
|
+
main().catch((e) => {
|
|
308
|
+
console.log(`\n ${RED}✖${RESET} ${WHITE}${e.message}${RESET}\n`);
|
|
385
309
|
exit(1);
|
|
386
310
|
});
|