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 +9 -9
- package/package.json +1 -1
- package/src/chat.js +35 -27
- package/src/layout.js +17 -11
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
146
|
-
chalk.white(m.content.slice(0, inner() -
|
|
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(
|
|
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
package/src/chat.js
CHANGED
|
@@ -24,18 +24,27 @@ const P = {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
// ── Markdown ───────────────────────────────────────────────────────────────────
|
|
27
|
-
|
|
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('
|
|
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('
|
|
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(
|
|
93
|
+
process.stdout.write(boxRow(row1parts.join(P.dim(' · ')), P.border) + '\n');
|
|
85
94
|
}
|
|
86
95
|
if (cfg?.url) {
|
|
87
|
-
const maxUrl = inner() -
|
|
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(
|
|
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 =
|
|
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()
|
|
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(
|
|
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(
|
|
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('
|
|
220
|
-
process.stdout.write(boxLine(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('
|
|
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(
|
|
317
|
+
process.stdout.write(boxLine(l, P.border) + '\n');
|
|
310
318
|
}
|
|
311
319
|
|
|
312
320
|
} else if (replyType === 'plan') {
|
|
313
|
-
process.stdout.write(boxLine(
|
|
314
|
-
if (reply.summary) process.stdout.write(boxLine(
|
|
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(
|
|
320
|
-
if (s.purpose) process.stdout.write(boxLine('
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
29
|
-
const lbl
|
|
30
|
-
|
|
31
|
-
|
|
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 -
|
|
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
|
|
62
|
-
const pad
|
|
63
|
-
return borderFn('│') + ' ' + content + ' '.repeat(pad) +
|
|
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] */
|