ispbills-icli 4.0.0 → 4.0.1
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 +22 -23
- 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
|
@@ -69,7 +69,7 @@ export function printBanner(cfg, meta = {}) {
|
|
|
69
69
|
process.stdout.write(boxTop('', P.border) + '\n');
|
|
70
70
|
|
|
71
71
|
// Title row
|
|
72
|
-
const title = P.bold('
|
|
72
|
+
const title = P.bold('iCopilot') + P.dim(' · IspBills AI Network Engineer');
|
|
73
73
|
process.stdout.write(boxRow(title, P.border) + '\n');
|
|
74
74
|
|
|
75
75
|
// Divider
|
|
@@ -81,19 +81,19 @@ export function printBanner(cfg, meta = {}) {
|
|
|
81
81
|
if (cfg?.user?.name) row1parts.push(P.dim('user ') + P.white(cfg.user.name));
|
|
82
82
|
if (cfg?.user?.role) row1parts.push(P.dim('role ') + P.white(cfg.user.role));
|
|
83
83
|
if (row1parts.length) {
|
|
84
|
-
process.stdout.write(boxRow(
|
|
84
|
+
process.stdout.write(boxRow(row1parts.join(P.dim(' · ')), P.border) + '\n');
|
|
85
85
|
}
|
|
86
86
|
if (cfg?.url) {
|
|
87
|
-
const maxUrl = inner() -
|
|
87
|
+
const maxUrl = inner() - 6;
|
|
88
88
|
const url = cfg.url.length > maxUrl ? cfg.url.slice(0, maxUrl - 1) + '…' : cfg.url;
|
|
89
|
-
process.stdout.write(boxRow(
|
|
89
|
+
process.stdout.write(boxRow(P.dim('url ') + P.accent(url), P.border) + '\n');
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
// Divider
|
|
93
93
|
process.stdout.write(boxMid(P.border) + '\n');
|
|
94
94
|
|
|
95
95
|
// Hints row
|
|
96
|
-
const hints =
|
|
96
|
+
const hints = [
|
|
97
97
|
P.accentB('/') + P.muted(' commands'),
|
|
98
98
|
P.accentB('Tab') + P.muted(' autocomplete'),
|
|
99
99
|
P.accentB('↑↓') + P.muted(' history'),
|
|
@@ -124,7 +124,7 @@ export function printSlashMenu() {
|
|
|
124
124
|
process.stdout.write(boxTop('Commands', P.border) + '\n');
|
|
125
125
|
for (const { cmd: c, hint, desc } of SLASH_COMMANDS) {
|
|
126
126
|
const key = P.accentB(c) + (hint ? P.dim(' ' + hint) : '');
|
|
127
|
-
const line =
|
|
127
|
+
const line = padRight(key, 22) + ' ' + P.dim(desc);
|
|
128
128
|
process.stdout.write(boxRow(line, P.border) + '\n');
|
|
129
129
|
}
|
|
130
130
|
process.stdout.write(boxBot(P.border) + '\n\n');
|
|
@@ -143,18 +143,18 @@ export function printUserBox(question) {
|
|
|
143
143
|
process.stdout.write('\n');
|
|
144
144
|
process.stdout.write(boxTop('You', P.accentB) + '\n');
|
|
145
145
|
// Word-wrap the question to fit inside the box
|
|
146
|
-
const maxW = inner()
|
|
146
|
+
const maxW = inner();
|
|
147
147
|
const words = question.split(' ');
|
|
148
148
|
let line = '';
|
|
149
149
|
for (const word of words) {
|
|
150
150
|
if (line.length + word.length + 1 > maxW && line) {
|
|
151
|
-
process.stdout.write(boxRow(
|
|
151
|
+
process.stdout.write(boxRow(P.white(line), P.accentB) + '\n');
|
|
152
152
|
line = word;
|
|
153
153
|
} else {
|
|
154
154
|
line = line ? line + ' ' + word : word;
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
-
if (line) process.stdout.write(boxRow(
|
|
157
|
+
if (line) process.stdout.write(boxRow(P.white(line), P.accentB) + '\n');
|
|
158
158
|
process.stdout.write(boxBot(P.accentB) + '\n');
|
|
159
159
|
}
|
|
160
160
|
|
|
@@ -216,15 +216,15 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
216
216
|
stopSpinner();
|
|
217
217
|
reasoningOpen = true;
|
|
218
218
|
process.stdout.write(boxLine('', P.border) + '\n');
|
|
219
|
-
process.stdout.write(boxLine(P.dim('
|
|
220
|
-
process.stdout.write(boxLine(P.border('
|
|
219
|
+
process.stdout.write(boxLine(P.dim('╌ Reasoning ') + P.border('╌'.repeat(Math.max(0, inner() - 14))), P.border) + '\n');
|
|
220
|
+
process.stdout.write(boxLine(P.border('│ '), P.border));
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
function writeReasonDelta(delta) {
|
|
224
224
|
if (!reasoningOpen) return;
|
|
225
225
|
const parts = delta.split('\n');
|
|
226
226
|
for (let i = 0; i < parts.length; i++) {
|
|
227
|
-
if (i > 0) process.stdout.write('\n' + boxLine(P.border('
|
|
227
|
+
if (i > 0) process.stdout.write('\n' + boxLine(P.border('│ '), P.border));
|
|
228
228
|
if (parts[i]) process.stdout.write(P.reasoning(parts[i]));
|
|
229
229
|
}
|
|
230
230
|
}
|
|
@@ -241,7 +241,7 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
241
241
|
stopSpinner();
|
|
242
242
|
const icon = statusIcon(line);
|
|
243
243
|
const text = line.trimStart();
|
|
244
|
-
const indent = /^[\s·•⌁]/.test(line) ? '
|
|
244
|
+
const indent = /^[\s·•⌁]/.test(line) ? ' ' : '';
|
|
245
245
|
process.stdout.write(boxLine(indent + icon + ' ' + P.dim(text), P.border) + '\n');
|
|
246
246
|
if (showThinking) spinner.start();
|
|
247
247
|
}
|
|
@@ -304,38 +304,37 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
304
304
|
if (replyType === 'message') {
|
|
305
305
|
const text = reply.text || answerBuf || '(no response)';
|
|
306
306
|
const rendered = renderMarkdown(text);
|
|
307
|
-
// Render each markdown line with │ prefix
|
|
308
307
|
for (const l of rendered.split('\n')) {
|
|
309
|
-
process.stdout.write(boxLine(
|
|
308
|
+
process.stdout.write(boxLine(l, P.border) + '\n');
|
|
310
309
|
}
|
|
311
310
|
|
|
312
311
|
} else if (replyType === 'plan') {
|
|
313
|
-
process.stdout.write(boxLine(
|
|
314
|
-
if (reply.summary) process.stdout.write(boxLine(
|
|
312
|
+
process.stdout.write(boxLine(P.bold('Plan'), P.border) + '\n');
|
|
313
|
+
if (reply.summary) process.stdout.write(boxLine(P.dim(reply.summary), P.border) + '\n');
|
|
315
314
|
process.stdout.write(boxLine('', P.border) + '\n');
|
|
316
315
|
(reply.steps || []).forEach((s, i) => {
|
|
317
316
|
const risk = s.risk === 'high' ? P.err(' ⚠ high risk') :
|
|
318
317
|
s.risk === 'medium' ? P.warn(' ⚡') : '';
|
|
319
|
-
process.stdout.write(boxLine(
|
|
320
|
-
if (s.purpose) process.stdout.write(boxLine('
|
|
318
|
+
process.stdout.write(boxLine(`${P.dim(String(i+1)+'.')} ${P.warn(s.cmd || '')}${risk}`, P.border) + '\n');
|
|
319
|
+
if (s.purpose) process.stdout.write(boxLine(' ' + P.muted(s.purpose), P.border) + '\n');
|
|
321
320
|
});
|
|
322
321
|
process.stdout.write(boxLine('', P.border) + '\n');
|
|
323
|
-
process.stdout.write(boxLine(
|
|
322
|
+
process.stdout.write(boxLine(P.dim('Type ') + P.white('apply fix') + P.dim(' to confirm.'), P.border) + '\n');
|
|
324
323
|
|
|
325
324
|
} else if (replyType === 'connect') {
|
|
326
325
|
const dev = reply.device ?? {};
|
|
327
326
|
process.stdout.write(
|
|
328
|
-
boxLine(
|
|
327
|
+
boxLine(P.ok('🔌 ') + P.bold(`${dev.kind ?? '?'}#${dev.id ?? '?'}`) +
|
|
329
328
|
' ' + P.white(dev.name ?? '') + ' ' + P.dim(`(${dev.host ?? ''})`), P.border) + '\n'
|
|
330
329
|
);
|
|
331
|
-
if (reply.note) process.stdout.write(boxLine(
|
|
330
|
+
if (reply.note) process.stdout.write(boxLine(P.dim(reply.note), P.border) + '\n');
|
|
332
331
|
}
|
|
333
332
|
|
|
334
333
|
// Blank + footer line inside box
|
|
335
334
|
process.stdout.write(boxLine('', P.border) + '\n');
|
|
336
335
|
const foot = [meta.model ? shortModel(meta.model) : null, meta.user, meta.role].filter(Boolean);
|
|
337
336
|
if (foot.length) {
|
|
338
|
-
process.stdout.write(boxLine(
|
|
337
|
+
process.stdout.write(boxLine(P.muted(foot.join(' · ')), P.border) + '\n');
|
|
339
338
|
}
|
|
340
339
|
|
|
341
340
|
// 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] */
|