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 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}✓${RESET} iCli updated. ${DIM}Restart iCli to use the new version.${RESET}\n\n`);
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}✖${RESET} Update failed. Try manually:\n`);
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}✓${RESET} Logged in as ${WHITE}${cfg.user?.name ?? cfg.user?.email}${RESET}` +
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}❯${RESET} ${DIM}${question}${RESET}`);
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}✖${RESET} ${WHITE}${e.message}${RESET}\n`);
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}❯${RESET} `;
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}✓${RESET} ${DIM}New conversation started.${RESET}\n`); rl.prompt(); return;
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 + '✓' + RESET + ' Logged out — credentials cleared.' : YELLOW + 'Already logged out.' + RESET}`);
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}✓${RESET} Logged in as ${WHITE}${cfg.user?.name ?? cfg.user?.email}${RESET}${DIM} · ${cfg.user?.role ?? 'operator'}${RESET}`);
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}✓${RESET} Logged out — credentials cleared.`);
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}✖${RESET} ${WHITE}${e.message}${RESET}\n`);
308
+ console.log(`\n ${RED}${glyphs.cross}${RESET} ${WHITE}${e.message}${RESET}\n`);
309
309
  exit(1);
310
310
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ispbills-icli",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
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 >= 62) {
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
@@ -1,6 +1,6 @@
1
- import { DIM, RESET } from './ui.js';
1
+ import { DIM, RESET, SPINNER } from './ui.js';
2
2
 
3
- const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
3
+ const SPINNER_FRAMES = SPINNER;
4
4
 
5
5
  const GRADIENT_COLORS = [
6
6
  '\x1b[38;5;240m',
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}✗${RESET}`;
10
- if (/done|success|found|complet|online|ready|ok\b/.test(l)) return `${GREEN}✓${RESET}`;
11
- if (/batch|sub.?agent|fleet|parallel/.test(l)) return `${ACCENT}◈${RESET}`;
12
- if (/\[\d+\/\d+\]/.test(l)) return `${ACCENT}◉${RESET}`;
13
- if (/probe|reachab|ping/.test(l)) return `${YELLOW}◎${RESET}`;
14
- if (/connect|ssh|telnet|api|session/.test(l)) return `${ACCENT}⇢${RESET}`;
15
- if (/read|fetch|get|query|look|search/.test(l)) return `${GRAY}↓${RESET}`;
16
- return `${GRAY}·${RESET}`;
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}·${RESET} `);
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}◆${RESET} ${DIM}Reasoning${RESET}\n ${GRAY}`);
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} Plan${RESET}`);
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 ? '└─' : '├─'}${RESET}`;
134
+ const branch = `${GRAY}${last ? glyphs.branchEnd : glyphs.branchMid}${RESET}`;
135
135
  const risk =
136
- (s.risk === 'destructive' || s.risk === 'high') ? ` ${RED} destructive${RESET}` :
137
- (s.risk === 'caution' || s.risk === 'medium') ? ` ${YELLOW} caution${RESET}` : '';
138
- const blocked = s.blocked ? ` ${RED} blocked${RESET}` : '';
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}🔌${RESET} ${BOLD}${(d.kind ?? '?').toUpperCase()} #${d.id ?? '?'}${RESET}${proto}`
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(`\n ${GRAY}${parts.join(' · ')}${RESET}`);
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 '';