ispbills-icli 4.0.0 → 4.0.3

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/bin/icli.js CHANGED
@@ -31,7 +31,7 @@ function printHelp() {
31
31
  process.stdout.write(boxTop('iCli · Help', P.border) + '\n');
32
32
 
33
33
  process.stdout.write(boxRow('', P.border) + '\n');
34
- process.stdout.write(boxRow(' ' + P.bold('Usage'), P.border) + '\n');
34
+ process.stdout.write(boxRow(P.bold('Usage'), P.border) + '\n');
35
35
  process.stdout.write(boxRow('', P.border) + '\n');
36
36
  [
37
37
  ['icli', 'Interactive REPL with slash commands'],
@@ -40,19 +40,19 @@ function printHelp() {
40
40
  ['icli whoami', 'Show current session info'],
41
41
  ['icli --help', 'Show this help'],
42
42
  ].forEach(([c, d]) =>
43
- process.stdout.write(boxRow(' ' + P.accent(c.padEnd(20)) + P.dim(d), P.border) + '\n')
43
+ process.stdout.write(boxRow(P.accent(c.padEnd(20)) + P.dim(d), P.border) + '\n')
44
44
  );
45
45
 
46
46
  process.stdout.write(boxMid(P.border) + '\n');
47
- process.stdout.write(boxRow(' ' + P.bold('Slash commands'), P.border) + '\n');
47
+ process.stdout.write(boxRow(P.bold('Slash commands'), P.border) + '\n');
48
48
  process.stdout.write(boxRow('', P.border) + '\n');
49
49
  for (const { cmd: c, hint, desc } of SLASH_COMMANDS) {
50
50
  const key = (c + (hint ? ' '+hint : '')).padEnd(22);
51
- process.stdout.write(boxRow(' ' + P.accent(key) + P.dim(desc), P.border) + '\n');
51
+ process.stdout.write(boxRow(P.accent(key) + P.dim(desc), P.border) + '\n');
52
52
  }
53
53
 
54
54
  process.stdout.write(boxMid(P.border) + '\n');
55
- process.stdout.write(boxRow(' ' + P.bold('Keyboard shortcuts'), P.border) + '\n');
55
+ process.stdout.write(boxRow(P.bold('Keyboard shortcuts'), P.border) + '\n');
56
56
  process.stdout.write(boxRow('', P.border) + '\n');
57
57
  [
58
58
  ['Tab', 'Autocomplete slash commands'],
@@ -61,7 +61,7 @@ function printHelp() {
61
61
  ['Ctrl+L', 'Clear screen'],
62
62
  ['Ctrl+C', 'Cancel / exit'],
63
63
  ].forEach(([k, d]) =>
64
- process.stdout.write(boxRow(' ' + P.warn(k.padEnd(14)) + P.dim(d), P.border) + '\n')
64
+ process.stdout.write(boxRow(P.warn(k.padEnd(14)) + P.dim(d), P.border) + '\n')
65
65
  );
66
66
  process.stdout.write(boxRow('', P.border) + '\n');
67
67
  process.stdout.write(boxBot(P.border) + '\n\n');
@@ -142,8 +142,8 @@ async function interactiveRepl(cfg) {
142
142
  } else {
143
143
  process.stdout.write('\n' + boxTop('History', P.border) + '\n');
144
144
  turns.forEach((m, i) =>
145
- process.stdout.write(boxRow(' ' + P.dim(String(i+1).padStart(2)+'.') + ' ' +
146
- chalk.white(m.content.slice(0, inner() - 8)), P.border) + '\n')
145
+ process.stdout.write(boxRow(P.dim(String(i+1).padStart(2)+'.') + ' ' +
146
+ chalk.white(m.content.slice(0, inner() - 5)), P.border) + '\n')
147
147
  );
148
148
  process.stdout.write(boxBot(P.border) + '\n\n');
149
149
  }
@@ -203,7 +203,7 @@ async function main() {
203
203
  [['url', cfg.url], ['name', cfg.user?.name], ['email', cfg.user?.email], ['role', cfg.user?.role]]
204
204
  .filter(([, v]) => v)
205
205
  .forEach(([k, v]) =>
206
- process.stdout.write(boxRow(' ' + P.dim(k.padEnd(7)) + chalk.white(v), P.border) + '\n')
206
+ process.stdout.write(boxRow(P.dim(k.padEnd(7)) + chalk.white(v), P.border) + '\n')
207
207
  );
208
208
  process.stdout.write(boxBot(P.border) + '\n\n');
209
209
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ispbills-icli",
3
- "version": "4.0.0",
3
+ "version": "4.0.3",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
package/src/chat.js CHANGED
@@ -24,18 +24,27 @@ const P = {
24
24
  };
25
25
 
26
26
  // ── Markdown ───────────────────────────────────────────────────────────────────
27
- marked.setOptions({ renderer: new TerminalRenderer({
27
+ const termRenderer = new TerminalRenderer({
28
28
  firstHeading: chalk.bold.white,
29
29
  heading: chalk.bold.white,
30
30
  strong: chalk.bold.white,
31
31
  em: chalk.italic.hex('#9198A1'),
32
32
  codespan: (t) => chalk.bgHex('#161b22').hex('#c9d1d9')(` ${t} `),
33
33
  code: (t) => '\n' + t.split('\n').map(l => ' ' + chalk.hex('#a5d6ff')(l)).join('\n') + '\n',
34
- blockquote: (t) => P.dim(' ▌ ') + P.dim(t.trim()),
35
- listitem: (t) => ` ${P.accent('·')} ${t.trimEnd()}`,
34
+ blockquote: (t) => P.dim('▌ ') + P.dim(t.trim()) + '\n',
36
35
  hr: () => P.border('─'.repeat(inner())) + '\n',
37
36
  link: (_h, _t, t) => chalk.hex('#58a6ff').underline(t || _h),
38
- }) });
37
+ });
38
+
39
+ // Override listitem on the instance — the options.listitem is a chalk color, not a renderer fn
40
+ const _origListitem = termRenderer.listitem.bind(termRenderer);
41
+ termRenderer.listitem = function(item) {
42
+ const raw = _origListitem(item);
43
+ // Replace the default ' * ' or ' - ' bullet with our accent '·'
44
+ return raw.replace(/^\s*[*\-]\s/, (m) => m.replace(/[*\-]/, P.accent('·')));
45
+ };
46
+
47
+ marked.setOptions({ renderer: termRenderer });
39
48
 
40
49
  function renderMarkdown(text) {
41
50
  try { return marked(text).trimEnd(); } catch { return text; }
@@ -69,7 +78,7 @@ export function printBanner(cfg, meta = {}) {
69
78
  process.stdout.write(boxTop('', P.border) + '\n');
70
79
 
71
80
  // Title row
72
- const title = P.bold(' iCopilot') + P.dim(' · IspBills AI Network Engineer');
81
+ const title = P.bold('iCopilot') + P.dim(' · IspBills AI Network Engineer');
73
82
  process.stdout.write(boxRow(title, P.border) + '\n');
74
83
 
75
84
  // Divider
@@ -81,19 +90,19 @@ export function printBanner(cfg, meta = {}) {
81
90
  if (cfg?.user?.name) row1parts.push(P.dim('user ') + P.white(cfg.user.name));
82
91
  if (cfg?.user?.role) row1parts.push(P.dim('role ') + P.white(cfg.user.role));
83
92
  if (row1parts.length) {
84
- process.stdout.write(boxRow(' ' + row1parts.join(P.dim(' · ')), P.border) + '\n');
93
+ process.stdout.write(boxRow(row1parts.join(P.dim(' · ')), P.border) + '\n');
85
94
  }
86
95
  if (cfg?.url) {
87
- const maxUrl = inner() - 8;
96
+ const maxUrl = inner() - 6;
88
97
  const url = cfg.url.length > maxUrl ? cfg.url.slice(0, maxUrl - 1) + '…' : cfg.url;
89
- process.stdout.write(boxRow(' ' + P.dim('url ') + P.accent(url), P.border) + '\n');
98
+ process.stdout.write(boxRow(P.dim('url ') + P.accent(url), P.border) + '\n');
90
99
  }
91
100
 
92
101
  // Divider
93
102
  process.stdout.write(boxMid(P.border) + '\n');
94
103
 
95
104
  // Hints row
96
- const hints = ' ' + [
105
+ const hints = [
97
106
  P.accentB('/') + P.muted(' commands'),
98
107
  P.accentB('Tab') + P.muted(' autocomplete'),
99
108
  P.accentB('↑↓') + P.muted(' history'),
@@ -124,7 +133,7 @@ export function printSlashMenu() {
124
133
  process.stdout.write(boxTop('Commands', P.border) + '\n');
125
134
  for (const { cmd: c, hint, desc } of SLASH_COMMANDS) {
126
135
  const key = P.accentB(c) + (hint ? P.dim(' ' + hint) : '');
127
- const line = ' ' + padRight(key, 22) + ' ' + P.dim(desc);
136
+ const line = padRight(key, 22) + ' ' + P.dim(desc);
128
137
  process.stdout.write(boxRow(line, P.border) + '\n');
129
138
  }
130
139
  process.stdout.write(boxBot(P.border) + '\n\n');
@@ -143,18 +152,18 @@ export function printUserBox(question) {
143
152
  process.stdout.write('\n');
144
153
  process.stdout.write(boxTop('You', P.accentB) + '\n');
145
154
  // Word-wrap the question to fit inside the box
146
- const maxW = inner() - 2;
155
+ const maxW = inner();
147
156
  const words = question.split(' ');
148
157
  let line = '';
149
158
  for (const word of words) {
150
159
  if (line.length + word.length + 1 > maxW && line) {
151
- process.stdout.write(boxRow(' ' + P.white(line), P.accentB) + '\n');
160
+ process.stdout.write(boxRow(P.white(line), P.accentB) + '\n');
152
161
  line = word;
153
162
  } else {
154
163
  line = line ? line + ' ' + word : word;
155
164
  }
156
165
  }
157
- if (line) process.stdout.write(boxRow(' ' + P.white(line), P.accentB) + '\n');
166
+ if (line) process.stdout.write(boxRow(P.white(line), P.accentB) + '\n');
158
167
  process.stdout.write(boxBot(P.accentB) + '\n');
159
168
  }
160
169
 
@@ -216,15 +225,15 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
216
225
  stopSpinner();
217
226
  reasoningOpen = true;
218
227
  process.stdout.write(boxLine('', P.border) + '\n');
219
- process.stdout.write(boxLine(P.dim(' ╌ Reasoning ') + P.border('╌'.repeat(Math.max(0, inner() - 16))), P.border) + '\n');
220
- process.stdout.write(boxLine(P.border(' │ '), P.border));
228
+ process.stdout.write(boxLine(P.dim('╌ Reasoning ') + P.border('╌'.repeat(Math.max(0, inner() - 14))), P.border) + '\n');
229
+ process.stdout.write(boxLine(P.border('│ '), P.border));
221
230
  }
222
231
 
223
232
  function writeReasonDelta(delta) {
224
233
  if (!reasoningOpen) return;
225
234
  const parts = delta.split('\n');
226
235
  for (let i = 0; i < parts.length; i++) {
227
- if (i > 0) process.stdout.write('\n' + boxLine(P.border(' │ '), P.border));
236
+ if (i > 0) process.stdout.write('\n' + boxLine(P.border('│ '), P.border));
228
237
  if (parts[i]) process.stdout.write(P.reasoning(parts[i]));
229
238
  }
230
239
  }
@@ -241,7 +250,7 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
241
250
  stopSpinner();
242
251
  const icon = statusIcon(line);
243
252
  const text = line.trimStart();
244
- const indent = /^[\s·•⌁]/.test(line) ? ' ' : ' ';
253
+ const indent = /^[\s·•⌁]/.test(line) ? ' ' : '';
245
254
  process.stdout.write(boxLine(indent + icon + ' ' + P.dim(text), P.border) + '\n');
246
255
  if (showThinking) spinner.start();
247
256
  }
@@ -304,38 +313,37 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
304
313
  if (replyType === 'message') {
305
314
  const text = reply.text || answerBuf || '(no response)';
306
315
  const rendered = renderMarkdown(text);
307
- // Render each markdown line with │ prefix
308
316
  for (const l of rendered.split('\n')) {
309
- process.stdout.write(boxLine(' ' + l, P.border) + '\n');
317
+ process.stdout.write(boxLine(l, P.border) + '\n');
310
318
  }
311
319
 
312
320
  } else if (replyType === 'plan') {
313
- process.stdout.write(boxLine(' ' + P.bold('Plan'), P.border) + '\n');
314
- if (reply.summary) process.stdout.write(boxLine(' ' + P.dim(reply.summary), P.border) + '\n');
321
+ process.stdout.write(boxLine(P.bold('Plan'), P.border) + '\n');
322
+ if (reply.summary) process.stdout.write(boxLine(P.dim(reply.summary), P.border) + '\n');
315
323
  process.stdout.write(boxLine('', P.border) + '\n');
316
324
  (reply.steps || []).forEach((s, i) => {
317
325
  const risk = s.risk === 'high' ? P.err(' ⚠ high risk') :
318
326
  s.risk === 'medium' ? P.warn(' ⚡') : '';
319
- process.stdout.write(boxLine(` ${P.dim(String(i+1)+'.')} ${P.warn(s.cmd || '')}${risk}`, P.border) + '\n');
320
- if (s.purpose) process.stdout.write(boxLine(' ' + P.muted(s.purpose), P.border) + '\n');
327
+ process.stdout.write(boxLine(`${P.dim(String(i+1)+'.')} ${P.warn(s.cmd || '')}${risk}`, P.border) + '\n');
328
+ if (s.purpose) process.stdout.write(boxLine(' ' + P.muted(s.purpose), P.border) + '\n');
321
329
  });
322
330
  process.stdout.write(boxLine('', P.border) + '\n');
323
- process.stdout.write(boxLine(' ' + P.dim('Type ') + P.white('apply fix') + P.dim(' to confirm.'), P.border) + '\n');
331
+ process.stdout.write(boxLine(P.dim('Type ') + P.white('apply fix') + P.dim(' to confirm.'), P.border) + '\n');
324
332
 
325
333
  } else if (replyType === 'connect') {
326
334
  const dev = reply.device ?? {};
327
335
  process.stdout.write(
328
- boxLine(' ' + P.ok('🔌 ') + P.bold(`${dev.kind ?? '?'}#${dev.id ?? '?'}`) +
336
+ boxLine(P.ok('🔌 ') + P.bold(`${dev.kind ?? '?'}#${dev.id ?? '?'}`) +
329
337
  ' ' + P.white(dev.name ?? '') + ' ' + P.dim(`(${dev.host ?? ''})`), P.border) + '\n'
330
338
  );
331
- if (reply.note) process.stdout.write(boxLine(' ' + P.dim(reply.note), P.border) + '\n');
339
+ if (reply.note) process.stdout.write(boxLine(P.dim(reply.note), P.border) + '\n');
332
340
  }
333
341
 
334
342
  // Blank + footer line inside box
335
343
  process.stdout.write(boxLine('', P.border) + '\n');
336
344
  const foot = [meta.model ? shortModel(meta.model) : null, meta.user, meta.role].filter(Boolean);
337
345
  if (foot.length) {
338
- process.stdout.write(boxLine(' ' + P.muted(foot.join(' · ')), P.border) + '\n');
346
+ process.stdout.write(boxLine(P.muted(foot.join(' · ')), P.border) + '\n');
339
347
  }
340
348
 
341
349
  // Close the box
package/src/layout.js CHANGED
@@ -23,12 +23,17 @@ export const padRight = (s, n) => s + ' '.repeat(Math.max(0, n - visLen(s)));
23
23
  // draw(color, char, n) — repeat char n times with color
24
24
  const rep = (ch, n) => ch.repeat(Math.max(0, n));
25
25
 
26
- /** Full top border: ╭─ [label] ──────────────╮ */
26
+ /** Full top border: ╭─ [label] ──────────────╮
27
+ * ╭ (1) + ─ (1) + lbl (lbl.len) + dash + ╮ (1) = w
28
+ * → dash = w - 3 - lbl.length */
27
29
  export function boxTop(label, borderFn) {
28
- const w = cols();
29
- const lbl = label ? ` ${label} ` : '';
30
- const dash = rep('─', Math.max(0, w - 2 - lbl.length));
31
- return borderFn(`╭${lbl.length ? '─' + lbl : ''}${dash}╮`);
30
+ const w = cols();
31
+ const lbl = label ? ` ${label} ` : '';
32
+ if (lbl.length) {
33
+ const dash = rep('─', Math.max(0, w - 3 - lbl.length));
34
+ return borderFn(`╭─${lbl}${dash}╮`);
35
+ }
36
+ return borderFn('╭' + rep('─', w - 2) + '╮');
32
37
  }
33
38
 
34
39
  /** Divider inside box: ├────────────────────────┤ */
@@ -42,11 +47,11 @@ export function boxBot(borderFn) {
42
47
  }
43
48
 
44
49
  /** Open top border (no right cap — for streaming sections):
45
- * ╭─ [label] ────────── */
50
+ * ╭─ [label] ────────────────────────────── */
46
51
  export function boxTopOpen(label, borderFn) {
47
52
  const w = cols();
48
53
  const lbl = label ? ` ${label} ` : '';
49
- const dash = rep('─', Math.max(0, w - 1 - lbl.length));
54
+ const dash = rep('─', Math.max(0, w - 2 - lbl.length));
50
55
  return borderFn(`╭─${lbl}${dash}`);
51
56
  }
52
57
 
@@ -56,11 +61,12 @@ export function boxBotOpen(borderFn) {
56
61
  }
57
62
 
58
63
  /** One content row with left border and padded right border:
59
- * │ [content] */
64
+ * │ [content]
65
+ * 1 + 2 + visLen(content) + pad + 1 = cols() → pad = cols()-4-visLen(content) */
60
66
  export function boxRow(content, borderFn) {
61
- const w = cols();
62
- const pad = Math.max(0, w - 4 - visLen(content));
63
- return borderFn('│') + ' ' + content + ' '.repeat(pad) + ' ' + borderFn('│');
67
+ const w = cols();
68
+ const pad = Math.max(1, w - 4 - visLen(content));
69
+ return borderFn('│') + ' ' + content + ' '.repeat(pad) + borderFn('│');
64
70
  }
65
71
 
66
72
  /** Left-border only line (for streaming): │ [content] */