ispbills-icli 6.0.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 +1 -0
- package/bin/icli.js +12 -12
- package/package.json +1 -1
- package/src/banner.js +8 -4
- package/src/loader.js +2 -2
- package/src/renderer.js +20 -18
- package/src/ui.js +31 -0
package/README.md
CHANGED
|
@@ -133,6 +133,7 @@ iCli connects to the same AI engine as the in-browser terminal:
|
|
|
133
133
|
- **Streaming output** — answers stream live with a thinking indicator, reasoning, and step-by-step server progress
|
|
134
134
|
- **Plans & connect cards** — proposed fixes render as a reviewable plan (`apply fix` to confirm); device connections show as a connect card
|
|
135
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
|
|
136
137
|
|
|
137
138
|
---
|
|
138
139
|
|
package/bin/icli.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
SLASH_COMMANDS, slashCompleter, slashToQuestion, printSlashMenu, printHistory,
|
|
17
17
|
} from '../src/commands.js';
|
|
18
18
|
import {
|
|
19
|
-
RESET, BOLD, DIM, GRAY, WHITE, ACCENT, GREEN, YELLOW, RED, CYAN, rule,
|
|
19
|
+
RESET, BOLD, DIM, GRAY, WHITE, ACCENT, GREEN, YELLOW, RED, CYAN, rule, glyphs,
|
|
20
20
|
} from '../src/ui.js';
|
|
21
21
|
|
|
22
22
|
const PKG_NAME = 'ispbills-icli';
|
|
@@ -78,10 +78,10 @@ function selfUpdate() {
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
if (res.status === 0) {
|
|
81
|
-
process.stdout.write(`\n ${GREEN}
|
|
81
|
+
process.stdout.write(`\n ${GREEN}${glyphs.check}${RESET} iCli updated. ${DIM}Restart iCli to use the new version.${RESET}\n\n`);
|
|
82
82
|
return true;
|
|
83
83
|
}
|
|
84
|
-
process.stdout.write(`\n ${RED}
|
|
84
|
+
process.stdout.write(`\n ${RED}${glyphs.cross}${RESET} Update failed. Try manually:\n`);
|
|
85
85
|
process.stdout.write(` ${ACCENT}npm install -g ${PKG_NAME}@latest${RESET}\n`);
|
|
86
86
|
process.stdout.write(` ${DIM}On some systems this needs elevated privileges (sudo / Administrator).${RESET}\n\n`);
|
|
87
87
|
return false;
|
|
@@ -132,7 +132,7 @@ async function ensureAuth() {
|
|
|
132
132
|
if (!cfg) {
|
|
133
133
|
process.stdout.write(`\n ${YELLOW}No saved credentials. Setting up iCli.${RESET}\n\n`);
|
|
134
134
|
cfg = await loginFlow();
|
|
135
|
-
process.stdout.write(`\n ${GREEN}
|
|
135
|
+
process.stdout.write(`\n ${GREEN}${glyphs.check}${RESET} Logged in as ${WHITE}${cfg.user?.name ?? cfg.user?.email}${RESET}` +
|
|
136
136
|
`${DIM} · ${cfg.user?.role ?? 'operator'}${RESET}\n\n`);
|
|
137
137
|
}
|
|
138
138
|
return cfg;
|
|
@@ -140,7 +140,7 @@ async function ensureAuth() {
|
|
|
140
140
|
|
|
141
141
|
// ── One conversation turn ───────────────────────────────────────────────────────
|
|
142
142
|
async function runTurn(cfg, history, renderer, loader, question, { echo = false } = {}) {
|
|
143
|
-
if (echo) console.log(`\n ${GRAY}
|
|
143
|
+
if (echo) console.log(`\n ${GRAY}${glyphs.prompt}${RESET} ${DIM}${question}${RESET}`);
|
|
144
144
|
history.push({ role: 'user', content: question });
|
|
145
145
|
|
|
146
146
|
const abort = new AbortController();
|
|
@@ -168,7 +168,7 @@ async function runTurn(cfg, history, renderer, loader, question, { echo = false
|
|
|
168
168
|
if (abort.signal.aborted) {
|
|
169
169
|
console.log(`\n ${DIM}Interrupted.${RESET}\n`);
|
|
170
170
|
} else {
|
|
171
|
-
console.log(`\n ${RED}
|
|
171
|
+
console.log(`\n ${RED}${glyphs.cross}${RESET} ${WHITE}${e.message}${RESET}\n`);
|
|
172
172
|
}
|
|
173
173
|
} finally {
|
|
174
174
|
process.removeListener('SIGINT', onSigint);
|
|
@@ -187,7 +187,7 @@ async function interactiveRepl(cfg) {
|
|
|
187
187
|
const history = [];
|
|
188
188
|
const renderer = new TuiRenderer({ display });
|
|
189
189
|
const loader = new Loader(display.loader);
|
|
190
|
-
const PROMPT = () => ` ${ACCENT}
|
|
190
|
+
const PROMPT = () => ` ${ACCENT}${glyphs.prompt}${RESET} `;
|
|
191
191
|
let busy = false;
|
|
192
192
|
|
|
193
193
|
printBanner(cfg, { model: cfg._model });
|
|
@@ -227,7 +227,7 @@ async function interactiveRepl(cfg) {
|
|
|
227
227
|
}
|
|
228
228
|
if (q === '/new') {
|
|
229
229
|
history.length = 0;
|
|
230
|
-
console.log(`\n ${GREEN}
|
|
230
|
+
console.log(`\n ${GREEN}${glyphs.check}${RESET} ${DIM}New conversation started.${RESET}\n`); rl.prompt(); return;
|
|
231
231
|
}
|
|
232
232
|
if (q === '/help' || q === 'help') { printHelp(); rl.prompt(); return; }
|
|
233
233
|
if (q === '/history' || q === 'history') { printHistory(history); rl.prompt(); return; }
|
|
@@ -237,7 +237,7 @@ async function interactiveRepl(cfg) {
|
|
|
237
237
|
}
|
|
238
238
|
if (q === '/logout' || q === 'logout') {
|
|
239
239
|
const cleared = clearConfig();
|
|
240
|
-
console.log(`\n ${cleared ? GREEN +
|
|
240
|
+
console.log(`\n ${cleared ? GREEN + glyphs.check + RESET + ' Logged out — credentials cleared.' : YELLOW + 'Already logged out.' + RESET}`);
|
|
241
241
|
console.log(` ${DIM}Run ${RESET}${ACCENT}icli login${RESET}${DIM} to sign in again.${RESET}\n`);
|
|
242
242
|
rl.close(); return;
|
|
243
243
|
}
|
|
@@ -264,7 +264,7 @@ async function main() {
|
|
|
264
264
|
|
|
265
265
|
if (cmd === 'login') {
|
|
266
266
|
const cfg = await loginFlow();
|
|
267
|
-
console.log(`\n ${GREEN}
|
|
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
268
|
console.log(` ${DIM}Config: ${configPath()}${RESET}\n`);
|
|
269
269
|
return;
|
|
270
270
|
}
|
|
@@ -274,7 +274,7 @@ async function main() {
|
|
|
274
274
|
if (cmd === 'logout') {
|
|
275
275
|
const cleared = clearConfig();
|
|
276
276
|
if (cleared) {
|
|
277
|
-
console.log(`\n ${GREEN}
|
|
277
|
+
console.log(`\n ${GREEN}${glyphs.check}${RESET} Logged out — credentials cleared.`);
|
|
278
278
|
console.log(` ${DIM}Run ${RESET}${ACCENT}icli login${RESET}${DIM} to sign in again.${RESET}\n`);
|
|
279
279
|
} else {
|
|
280
280
|
console.log(`\n ${YELLOW}Not logged in.${RESET} Nothing to clear.\n`);
|
|
@@ -305,6 +305,6 @@ async function main() {
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
main().catch((e) => {
|
|
308
|
-
console.log(`\n ${RED}
|
|
308
|
+
console.log(`\n ${RED}${glyphs.cross}${RESET} ${WHITE}${e.message}${RESET}\n`);
|
|
309
309
|
exit(1);
|
|
310
310
|
});
|
package/package.json
CHANGED
package/src/banner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RESET, BOLD, DIM, CYAN, WHITE, ACCENT, GRAY, cols, shortModel, rule } from './ui.js';
|
|
1
|
+
import { RESET, BOLD, DIM, CYAN, WHITE, ACCENT, GRAY, cols, shortModel, rule, hr } from './ui.js';
|
|
2
2
|
|
|
3
3
|
const LOGO = [
|
|
4
4
|
' ██╗ ██████╗ ██████╗ ██████╗ ██╗██╗ ██████╗ ████████╗',
|
|
@@ -9,8 +9,12 @@ const LOGO = [
|
|
|
9
9
|
' ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ',
|
|
10
10
|
];
|
|
11
11
|
|
|
12
|
+
// Widest visible logo line — used to decide when to fall back to a compact header.
|
|
13
|
+
const LOGO_WIDTH = Math.max(...LOGO.map((l) => l.length));
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Full iCopilot banner. On narrow terminals falls back to a compact header.
|
|
17
|
+
* A full-width rule frames the header so it uses the whole window.
|
|
14
18
|
*
|
|
15
19
|
* @param {object} cfg Loaded config ({ url, user, _model }).
|
|
16
20
|
* @param {object} opts { model }
|
|
@@ -20,13 +24,11 @@ export function printBanner(cfg = {}, opts = {}) {
|
|
|
20
24
|
const model = opts.model || cfg._model || 'IspBills AI';
|
|
21
25
|
|
|
22
26
|
console.log();
|
|
23
|
-
if (width >=
|
|
27
|
+
if (width >= LOGO_WIDTH) {
|
|
24
28
|
for (const line of LOGO) console.log(ACCENT + BOLD + line + RESET);
|
|
25
29
|
console.log(` ${DIM}iCopilot — network engineer in your terminal${RESET}`);
|
|
26
30
|
} else {
|
|
27
|
-
console.log(rule());
|
|
28
31
|
console.log(` ${BOLD}iCopilot${RESET} ${DIM}— IspBills AI terminal${RESET}`);
|
|
29
|
-
console.log(rule());
|
|
30
32
|
}
|
|
31
33
|
console.log();
|
|
32
34
|
|
|
@@ -39,4 +41,6 @@ export function printBanner(cfg = {}, opts = {}) {
|
|
|
39
41
|
console.log();
|
|
40
42
|
console.log(` ${DIM}Type a message to start. ${RESET}${ACCENT}/help${RESET}${DIM} for commands · ${RESET}${ACCENT}Ctrl+C${RESET}${DIM} to exit${RESET}`);
|
|
41
43
|
console.log();
|
|
44
|
+
hr();
|
|
45
|
+
console.log();
|
|
42
46
|
}
|
package/src/loader.js
CHANGED
package/src/renderer.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RESET, BOLD, DIM, ITALIC, CYAN, GREEN, YELLOW, RED, GRAY, WHITE, ACCENT,
|
|
3
|
-
cols, shortModel,
|
|
3
|
+
cols, shortModel, glyphs, hr,
|
|
4
4
|
} from './ui.js';
|
|
5
5
|
|
|
6
6
|
/** Pick a status icon from the content of a server progress line. */
|
|
7
7
|
function statusIcon(line) {
|
|
8
8
|
const l = line.toLowerCase();
|
|
9
|
-
if (/unreachable|skip|fail|error|denied|timeout/.test(l)) return `${RED}
|
|
10
|
-
if (/done|success|found|complet|online|ready|ok\b/.test(l)) return `${GREEN}
|
|
11
|
-
if (/batch|sub.?agent|fleet|parallel/.test(l)) return `${ACCENT}
|
|
12
|
-
if (/\[\d+\/\d+\]/.test(l)) return `${ACCENT}
|
|
13
|
-
if (/probe|reachab|ping/.test(l)) return `${YELLOW}
|
|
14
|
-
if (/connect|ssh|telnet|api|session/.test(l)) return `${ACCENT}
|
|
15
|
-
if (/read|fetch|get|query|look|search/.test(l)) return `${GRAY}
|
|
16
|
-
return `${GRAY}
|
|
9
|
+
if (/unreachable|skip|fail|error|denied|timeout/.test(l)) return `${RED}${glyphs.cross}${RESET}`;
|
|
10
|
+
if (/done|success|found|complet|online|ready|ok\b/.test(l)) return `${GREEN}${glyphs.check}${RESET}`;
|
|
11
|
+
if (/batch|sub.?agent|fleet|parallel/.test(l)) return `${ACCENT}${glyphs.ring}${RESET}`;
|
|
12
|
+
if (/\[\d+\/\d+\]/.test(l)) return `${ACCENT}${glyphs.diamond}${RESET}`;
|
|
13
|
+
if (/probe|reachab|ping/.test(l)) return `${YELLOW}${glyphs.target}${RESET}`;
|
|
14
|
+
if (/connect|ssh|telnet|api|session/.test(l)) return `${ACCENT}${glyphs.arrow}${RESET}`;
|
|
15
|
+
if (/read|fetch|get|query|look|search/.test(l)) return `${GRAY}${glyphs.down}${RESET}`;
|
|
16
|
+
return `${GRAY}${glyphs.bullet}${RESET}`;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -67,7 +67,7 @@ export class TuiRenderer {
|
|
|
67
67
|
let out = line
|
|
68
68
|
.replace(/\*\*(.+?)\*\*/g, `${BOLD}$1${RESET}`)
|
|
69
69
|
.replace(/`([^`]+)`/g, `${CYAN}$1${RESET}`)
|
|
70
|
-
.replace(/^(\s*)[-*]\s+/, `$1${ACCENT}
|
|
70
|
+
.replace(/^(\s*)[-*]\s+/, `$1${ACCENT}${glyphs.bullet}${RESET} `);
|
|
71
71
|
return ' ' + out;
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -89,7 +89,7 @@ export class TuiRenderer {
|
|
|
89
89
|
if (!this.display.reasoning) return;
|
|
90
90
|
this.endStreaming();
|
|
91
91
|
if (!this.reasoningOpen) {
|
|
92
|
-
process.stdout.write(`\n ${ACCENT}
|
|
92
|
+
process.stdout.write(`\n ${ACCENT}${glyphs.reasoning}${RESET} ${DIM}Reasoning${RESET}\n ${GRAY}`);
|
|
93
93
|
this.reasoningOpen = true;
|
|
94
94
|
}
|
|
95
95
|
process.stdout.write(delta.replace(/\n/g, `\n ${GRAY}`));
|
|
@@ -126,16 +126,16 @@ export class TuiRenderer {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
renderPlan(reply) {
|
|
129
|
-
console.log(`\n ${ACCENT}${BOLD}
|
|
129
|
+
console.log(`\n ${ACCENT}${BOLD}${glyphs.diamond} Plan${RESET}`);
|
|
130
130
|
if (reply.summary) console.log(` ${DIM}${reply.summary}${RESET}`);
|
|
131
131
|
const steps = reply.steps || [];
|
|
132
132
|
steps.forEach((s, i) => {
|
|
133
133
|
const last = i === steps.length - 1;
|
|
134
|
-
const branch = `${GRAY}${last ?
|
|
134
|
+
const branch = `${GRAY}${last ? glyphs.branchEnd : glyphs.branchMid}${RESET}`;
|
|
135
135
|
const risk =
|
|
136
|
-
(s.risk === 'destructive' || s.risk === 'high') ? ` ${RED}
|
|
137
|
-
(s.risk === 'caution' || s.risk === 'medium') ? ` ${YELLOW}
|
|
138
|
-
const blocked = s.blocked ? ` ${RED}
|
|
136
|
+
(s.risk === 'destructive' || s.risk === 'high') ? ` ${RED}${glyphs.warn} destructive${RESET}` :
|
|
137
|
+
(s.risk === 'caution' || s.risk === 'medium') ? ` ${YELLOW}${glyphs.bolt} caution${RESET}` : '';
|
|
138
|
+
const blocked = s.blocked ? ` ${RED}${glyphs.block} blocked${RESET}` : '';
|
|
139
139
|
console.log(` ${branch} ${DIM}${i + 1}.${RESET} ${YELLOW}${s.cmd || ''}${RESET}${risk}${blocked}`);
|
|
140
140
|
const why = s.why || s.purpose;
|
|
141
141
|
if (why) console.log(` ${GRAY}${why}${RESET}`);
|
|
@@ -147,7 +147,7 @@ export class TuiRenderer {
|
|
|
147
147
|
const d = reply.device ?? reply ?? {};
|
|
148
148
|
const proto = d.proto ? ` ${DIM}via ${d.proto}${RESET}` : '';
|
|
149
149
|
console.log(
|
|
150
|
-
`\n ${GREEN}
|
|
150
|
+
`\n ${GREEN}${glyphs.plug}${RESET} ${BOLD}${(d.kind ?? '?').toUpperCase()} #${d.id ?? '?'}${RESET}${proto}`
|
|
151
151
|
);
|
|
152
152
|
if (reply.note) console.log(` ${DIM}${reply.note}${RESET}`);
|
|
153
153
|
console.log(` ${DIM}Establishing session…${RESET}`);
|
|
@@ -155,8 +155,10 @@ export class TuiRenderer {
|
|
|
155
155
|
|
|
156
156
|
renderFooter(meta) {
|
|
157
157
|
const parts = [meta.model ? shortModel(meta.model) : null, meta.user, meta.role].filter(Boolean);
|
|
158
|
+
console.log();
|
|
159
|
+
hr();
|
|
158
160
|
if (parts.length) {
|
|
159
|
-
console.log(
|
|
161
|
+
console.log(` ${GRAY}${parts.join(' · ')}${RESET}`);
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
164
|
}
|
package/src/ui.js
CHANGED
|
@@ -53,6 +53,37 @@ export function rule(color = GRAY) {
|
|
|
53
53
|
return paint(color, '─'.repeat(cols()));
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// ── Glyphs ─────────────────────────────────────────────────────────────────
|
|
57
|
+
// Classic Windows consoles (cmd.exe / legacy PowerShell using the Consolas or
|
|
58
|
+
// raster font) lack many Unicode dingbats, geometric shapes, arrows, braille
|
|
59
|
+
// and emoji — they render as "tofu" boxes. Windows Terminal sets WT_SESSION,
|
|
60
|
+
// so we only fall back to ASCII on a classic Windows console.
|
|
61
|
+
const ASCII_SAFE = process.platform === 'win32' && !process.env.WT_SESSION;
|
|
62
|
+
|
|
63
|
+
export const glyphs = ASCII_SAFE
|
|
64
|
+
? {
|
|
65
|
+
prompt: '>', bullet: '-', diamond: '*', ring: '*', target: 'o',
|
|
66
|
+
arrow: '->', down: 'v', check: 'OK', cross: 'XX', warn: '!!',
|
|
67
|
+
bolt: '!', block: 'X', plug: '>>', reasoning: '~',
|
|
68
|
+
branchMid: '|-', branchEnd: '`-', vbar: '|',
|
|
69
|
+
}
|
|
70
|
+
: {
|
|
71
|
+
prompt: '❯', bullet: '·', diamond: '◆', ring: '◈', target: '◎',
|
|
72
|
+
arrow: '⇢', down: '↓', check: '✓', cross: '✗', warn: '⚠',
|
|
73
|
+
bolt: '⚡', block: '⛔', plug: '🔌', reasoning: '◆',
|
|
74
|
+
branchMid: '├─', branchEnd: '└─', vbar: '│',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Spinner frames — braille on modern terminals, ASCII on classic Windows. */
|
|
78
|
+
export const SPINNER = ASCII_SAFE
|
|
79
|
+
? ['|', '/', '-', '\\']
|
|
80
|
+
: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
81
|
+
|
|
82
|
+
/** Print a full-width horizontal rule (dim by default). */
|
|
83
|
+
export function hr(color = GRAY) {
|
|
84
|
+
process.stdout.write(paint(color, '─'.repeat(cols())) + '\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
56
87
|
/** Shorten a fully-qualified model id for display (e.g. "anthropic/claude-…"). */
|
|
57
88
|
export function shortModel(m) {
|
|
58
89
|
if (!m) return '';
|