ispbills-icli 1.0.1 → 2.0.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/bin/icli.js +75 -44
- package/package.json +5 -2
- package/src/chat.js +98 -62
package/bin/icli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { stdin as input, stdout as output, argv, exit } from 'process';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { loadConfig, configPath } from '../src/config.js';
|
|
6
6
|
import { loginFlow } from '../src/auth.js';
|
|
7
|
-
import { streamChat } from '../src/chat.js';
|
|
7
|
+
import { streamChat, printBanner } from '../src/chat.js';
|
|
8
8
|
|
|
9
9
|
// ─── Parse args ─────────────────────────────────────────────────────────────
|
|
10
10
|
const args = argv.slice(2);
|
|
@@ -12,47 +12,54 @@ const cmd = args[0] ?? '';
|
|
|
12
12
|
|
|
13
13
|
// ─── Help ────────────────────────────────────────────────────────────────────
|
|
14
14
|
function printHelp() {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
15
|
+
const line = '─'.repeat(58);
|
|
16
|
+
console.log('\n' + chalk.cyan(' ╭' + line + '╮'));
|
|
17
|
+
console.log(chalk.cyan(' │ ') + chalk.bold.white('iCli') + chalk.dim(' — IspBills AI Terminal') + ' '.repeat(28) + chalk.cyan(' │'));
|
|
18
|
+
console.log(chalk.cyan(' ╰' + line + '╯') + '\n');
|
|
19
|
+
|
|
20
|
+
console.log(chalk.bold(' Usage\n'));
|
|
21
|
+
console.log(' ' + chalk.cyan('icli') + ' ' + chalk.dim('Interactive REPL'));
|
|
22
|
+
console.log(' ' + chalk.cyan('icli') + ' ' + chalk.yellow('"your question"') + ' ' + chalk.dim('Single-shot query'));
|
|
23
|
+
console.log(' ' + chalk.cyan('icli login') + ' ' + chalk.dim('Authenticate'));
|
|
24
|
+
console.log(' ' + chalk.cyan('icli whoami') + ' ' + chalk.dim('Show current login'));
|
|
25
|
+
console.log(' ' + chalk.cyan('icli --help') + ' ' + chalk.dim('Show this help'));
|
|
26
|
+
|
|
27
|
+
console.log('\n' + chalk.bold(' Examples\n'));
|
|
28
|
+
console.log(' ' + chalk.dim('$') + ' icli "is vlan 82 in use?"');
|
|
29
|
+
console.log(' ' + chalk.dim('$') + ' icli "show all ONUs on olt#1"');
|
|
30
|
+
console.log(' ' + chalk.dim('$') + ' icli "which customer is using IP 192.168.1.50?"');
|
|
31
|
+
console.log(' ' + chalk.dim('$') + ' icli' + chalk.dim(' # start interactive REPL'));
|
|
32
|
+
|
|
33
|
+
console.log('\n' + chalk.bold(' Config\n'));
|
|
34
|
+
console.log(' ' + chalk.dim(configPath()) + '\n');
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// ─── Ensure authenticated ────────────────────────────────────────────────────
|
|
38
38
|
async function ensureAuth() {
|
|
39
39
|
let cfg = loadConfig();
|
|
40
40
|
if (!cfg) {
|
|
41
|
-
console.log(chalk.yellow('No saved login found. Let\'s
|
|
41
|
+
console.log(chalk.yellow('\n No saved login found. Let\'s connect iCli.\n'));
|
|
42
42
|
cfg = await loginFlow();
|
|
43
|
-
console.log(chalk.green(
|
|
44
|
-
|
|
43
|
+
console.log('\n ' + chalk.green('✓') + ' Logged in as ' +
|
|
44
|
+
chalk.white(cfg.user?.name ?? cfg.user?.email) +
|
|
45
|
+
chalk.dim(' · ' + (cfg.user?.role ?? 'operator')));
|
|
46
|
+
console.log(' ' + chalk.dim('Connected to: ') + chalk.cyan(cfg.url) + '\n');
|
|
45
47
|
}
|
|
46
48
|
return cfg;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
// ─── Single-shot query ───────────────────────────────────────────────────────
|
|
50
52
|
async function singleShot(cfg, question) {
|
|
53
|
+
// Show the user's own message
|
|
54
|
+
process.stdout.write('\n ' + chalk.cyan('You ') + chalk.white(question) + '\n');
|
|
55
|
+
process.stdout.write(' ' + chalk.dim('─'.repeat(58)) + '\n');
|
|
56
|
+
process.stdout.write(' ' + chalk.bold.cyan('iCopilot') + '\n');
|
|
57
|
+
|
|
51
58
|
const messages = [{ role: 'user', content: question }];
|
|
52
59
|
try {
|
|
53
60
|
await streamChat(cfg, messages);
|
|
54
61
|
} catch (e) {
|
|
55
|
-
console.error(chalk.red('
|
|
62
|
+
console.error('\n ' + chalk.red('✖ ' + e.message) + '\n');
|
|
56
63
|
exit(1);
|
|
57
64
|
}
|
|
58
65
|
}
|
|
@@ -60,41 +67,54 @@ async function singleShot(cfg, question) {
|
|
|
60
67
|
// ─── Interactive REPL ────────────────────────────────────────────────────────
|
|
61
68
|
async function interactiveRepl(cfg) {
|
|
62
69
|
const history = [];
|
|
70
|
+
|
|
71
|
+
printBanner(cfg);
|
|
72
|
+
|
|
63
73
|
const rl = readline.createInterface({
|
|
64
74
|
input, output,
|
|
65
|
-
prompt: chalk.cyan('
|
|
75
|
+
prompt: chalk.cyan(' You ') + chalk.white(''),
|
|
66
76
|
});
|
|
67
77
|
|
|
68
|
-
console.log(
|
|
69
|
-
chalk.bold.cyan('\n iCli') + chalk.dim(' — IspBills AI terminal\n') +
|
|
70
|
-
chalk.dim(` Connected to: ${cfg.url}`) + '\n' +
|
|
71
|
-
chalk.dim(' Type your question. Press Ctrl+C or type "exit" to quit.\n')
|
|
72
|
-
);
|
|
73
78
|
rl.prompt();
|
|
74
79
|
|
|
75
80
|
rl.on('line', async (line) => {
|
|
76
81
|
const q = line.trim();
|
|
77
82
|
if (!q) { rl.prompt(); return; }
|
|
78
|
-
|
|
79
|
-
if (q === '
|
|
83
|
+
|
|
84
|
+
if (q === 'exit' || q === 'quit') {
|
|
85
|
+
console.log(chalk.dim('\n Goodbye.\n'));
|
|
86
|
+
rl.close();
|
|
87
|
+
exit(0);
|
|
88
|
+
}
|
|
89
|
+
if (q === 'clear') { console.clear(); printBanner(cfg); rl.prompt(); return; }
|
|
80
90
|
if (q === 'history') {
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
console.log('');
|
|
92
|
+
history.filter(m => m.role === 'user').forEach((m, i) => {
|
|
93
|
+
console.log(' ' + chalk.dim(String(i + 1) + '.') + ' ' + m.content.slice(0, 80));
|
|
83
94
|
});
|
|
95
|
+
console.log('');
|
|
84
96
|
rl.prompt();
|
|
85
97
|
return;
|
|
86
98
|
}
|
|
99
|
+
if (q === 'help') { printHelp(); rl.prompt(); return; }
|
|
87
100
|
|
|
88
101
|
rl.pause();
|
|
89
102
|
history.push({ role: 'user', content: q });
|
|
90
103
|
|
|
104
|
+
// Show iCopilot label before streaming response
|
|
105
|
+
process.stdout.write(' ' + chalk.dim('─'.repeat(58)) + '\n');
|
|
106
|
+
process.stdout.write(' ' + chalk.bold.cyan('iCopilot') + '\n');
|
|
107
|
+
|
|
91
108
|
try {
|
|
92
109
|
const answer = await streamChat(cfg, [...history]);
|
|
93
110
|
if (answer) history.push({ role: 'assistant', content: answer });
|
|
94
111
|
} catch (e) {
|
|
95
|
-
console.error(chalk.red('
|
|
112
|
+
console.error('\n ' + chalk.red('✖ ' + e.message) + '\n');
|
|
96
113
|
}
|
|
97
114
|
|
|
115
|
+
// Prompt for next message with label
|
|
116
|
+
process.stdout.write(' ' + chalk.dim('─'.repeat(58)) + '\n');
|
|
117
|
+
rl.setPrompt(chalk.cyan(' You ') + chalk.white(''));
|
|
98
118
|
rl.resume();
|
|
99
119
|
rl.prompt();
|
|
100
120
|
});
|
|
@@ -113,18 +133,26 @@ async function main() {
|
|
|
113
133
|
|
|
114
134
|
if (cmd === 'login') {
|
|
115
135
|
const cfg = await loginFlow();
|
|
116
|
-
console.log(chalk.green(
|
|
117
|
-
|
|
136
|
+
console.log('\n ' + chalk.green('✓') + ' Logged in as ' +
|
|
137
|
+
chalk.white(cfg.user?.name ?? cfg.user?.email) +
|
|
138
|
+
chalk.dim(' · ' + (cfg.user?.role ?? 'operator')));
|
|
139
|
+
console.log(' ' + chalk.dim('Config saved: ') + chalk.dim(configPath()) + '\n');
|
|
118
140
|
return;
|
|
119
141
|
}
|
|
120
142
|
|
|
121
143
|
if (cmd === 'whoami') {
|
|
122
144
|
const cfg = loadConfig();
|
|
123
|
-
if (!cfg) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
145
|
+
if (!cfg) {
|
|
146
|
+
console.log('\n ' + chalk.yellow('Not logged in.') + ' Run ' + chalk.cyan('icli login') + '\n');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const line = '─'.repeat(58);
|
|
150
|
+
console.log('\n' + chalk.dim(' ' + line));
|
|
151
|
+
console.log(' ' + chalk.dim('URL ') + chalk.cyan(cfg.url));
|
|
152
|
+
console.log(' ' + chalk.dim('Name ') + chalk.white(cfg.user?.name ?? '—'));
|
|
153
|
+
console.log(' ' + chalk.dim('Email ') + chalk.white(cfg.user?.email ?? '—'));
|
|
154
|
+
console.log(' ' + chalk.dim('Role ') + chalk.white(cfg.user?.role ?? '—'));
|
|
155
|
+
console.log(chalk.dim(' ' + line) + '\n');
|
|
128
156
|
return;
|
|
129
157
|
}
|
|
130
158
|
|
|
@@ -140,4 +168,7 @@ async function main() {
|
|
|
140
168
|
await interactiveRepl(cfg);
|
|
141
169
|
}
|
|
142
170
|
|
|
143
|
-
main().catch(e => {
|
|
171
|
+
main().catch(e => {
|
|
172
|
+
console.error('\n ' + chalk.red('✖ ' + e.message) + '\n');
|
|
173
|
+
exit(1);
|
|
174
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ispbills-icli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "iCli — IspBills AI network engineer in your terminal",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ispbills",
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
"README.md"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"chalk": "^5.3.0"
|
|
27
|
+
"chalk": "^5.3.0",
|
|
28
|
+
"marked": "^15.0.12",
|
|
29
|
+
"marked-terminal": "^7.3.0",
|
|
30
|
+
"ora": "^9.4.1"
|
|
28
31
|
}
|
|
29
32
|
}
|
package/src/chat.js
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { marked } from 'marked';
|
|
4
|
+
import TerminalRenderer from 'marked-terminal';
|
|
2
5
|
import { refreshToken } from './auth.js';
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
// Configure markdown renderer
|
|
8
|
+
marked.setOptions({ renderer: new TerminalRenderer({
|
|
9
|
+
firstHeading: chalk.bold.cyan,
|
|
10
|
+
heading: chalk.bold.cyan,
|
|
11
|
+
strong: chalk.bold.white,
|
|
12
|
+
em: chalk.italic.dim,
|
|
13
|
+
codespan: (code) => chalk.bgGray.white(` ${code} `),
|
|
14
|
+
code: (code) => code.split('\n').map(l => chalk.green(' ' + l)).join('\n'),
|
|
15
|
+
blockquote: chalk.dim,
|
|
16
|
+
listitem: (text) => ` ${chalk.cyan('•')} ${text}`,
|
|
17
|
+
hr: () => chalk.dim(' ' + '─'.repeat(56)) + '\n',
|
|
18
|
+
link: (_href, _title, text) => chalk.cyan.underline(text || _href),
|
|
19
|
+
}) });
|
|
20
|
+
|
|
21
|
+
function renderMarkdown(text) {
|
|
22
|
+
try {
|
|
23
|
+
return marked(text).trimEnd();
|
|
24
|
+
} catch {
|
|
25
|
+
return text;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
5
28
|
|
|
6
29
|
/**
|
|
7
30
|
* Stream a single chat turn to stdout.
|
|
@@ -14,7 +37,6 @@ const SPINNER_FRAMES = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','
|
|
|
14
37
|
*/
|
|
15
38
|
export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
16
39
|
const showThinking = opts.thinking !== false;
|
|
17
|
-
|
|
18
40
|
const body = JSON.stringify({ messages, context });
|
|
19
41
|
|
|
20
42
|
let res = await fetch(`${cfg.url}/api/v1/ai/chat/stream`, {
|
|
@@ -48,55 +70,44 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
48
70
|
throw new Error(`AI request failed (HTTP ${res.status}): ${txt.slice(0, 200)}`);
|
|
49
71
|
}
|
|
50
72
|
|
|
51
|
-
// ──
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
let lastWasNewline = true;
|
|
59
|
-
|
|
60
|
-
function clearSpinner() {
|
|
61
|
-
if (spinTimer) { clearInterval(spinTimer); spinTimer = null; }
|
|
62
|
-
if (thinkingLine) {
|
|
63
|
-
process.stdout.write('\r\x1b[K'); // clear the thinking line
|
|
64
|
-
thinkingLine = false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
73
|
+
// ── Spinner ──────────────────────────────────────────────────────────────
|
|
74
|
+
const spinner = ora({
|
|
75
|
+
text: chalk.dim('Thinking…'),
|
|
76
|
+
color: 'cyan',
|
|
77
|
+
spinner: 'dots',
|
|
78
|
+
prefixText: ' ',
|
|
79
|
+
});
|
|
67
80
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
let reasonBuf = '';
|
|
82
|
+
let answerBuf = '';
|
|
83
|
+
let rawAnswer = '';
|
|
84
|
+
let finalResult = null;
|
|
85
|
+
|
|
86
|
+
function stopSpinner() {
|
|
87
|
+
if (spinner.isSpinning) spinner.stop();
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
function printStatus(line) {
|
|
78
|
-
|
|
91
|
+
stopSpinner();
|
|
79
92
|
process.stdout.write(chalk.magenta(' › ') + chalk.dim(line) + '\n');
|
|
80
|
-
|
|
81
|
-
if (showThinking) startSpinner();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function printReasoning(delta) {
|
|
85
|
-
// Reasoning streams as a running line; collapse to a single dim block.
|
|
86
|
-
reasonBuf += delta;
|
|
93
|
+
if (showThinking) spinner.start();
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
function flushReasoning() {
|
|
90
|
-
if (!reasonBuf) return;
|
|
91
|
-
|
|
92
|
-
const lines = reasonBuf.trim().split('\n');
|
|
93
|
-
process.stdout.write(
|
|
97
|
+
if (!reasonBuf.trim()) return;
|
|
98
|
+
stopSpinner();
|
|
99
|
+
const lines = reasonBuf.trim().split('\n').filter(Boolean);
|
|
100
|
+
process.stdout.write('\n');
|
|
101
|
+
process.stdout.write(chalk.dim(' ┌─ Reasoning ' + '─'.repeat(44)) + '\n');
|
|
102
|
+
for (const l of lines) {
|
|
103
|
+
process.stdout.write(chalk.dim(' │ ') + chalk.dim.italic(l) + '\n');
|
|
104
|
+
}
|
|
105
|
+
process.stdout.write(chalk.dim(' └' + '─'.repeat(57)) + '\n');
|
|
94
106
|
reasonBuf = '';
|
|
95
|
-
lastWasNewline = true;
|
|
96
107
|
}
|
|
97
108
|
|
|
98
109
|
function parseStreamText(raw) {
|
|
99
|
-
const m = raw.match(/"text"\s*:\s*"((?:[^"\\]|\\.)*)
|
|
110
|
+
const m = raw.match(/"text"\s*:\s*"((?:[^"\\]|\\.)*)/);
|
|
100
111
|
if (!m) return raw.trimStart().startsWith('{') ? null : raw;
|
|
101
112
|
return m[1]
|
|
102
113
|
.replace(/\\n/g, '\n').replace(/\\t/g, '\t')
|
|
@@ -104,17 +115,15 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
104
115
|
}
|
|
105
116
|
|
|
106
117
|
// ── SSE reader ───────────────────────────────────────────────────────────
|
|
107
|
-
if (showThinking)
|
|
118
|
+
if (showThinking) spinner.start();
|
|
108
119
|
|
|
109
120
|
const decoder = new TextDecoder();
|
|
110
121
|
let sseBuffer = '';
|
|
111
|
-
let finalResult = null;
|
|
112
|
-
let rawAnswer = '';
|
|
113
122
|
|
|
114
123
|
for await (const chunk of res.body) {
|
|
115
124
|
sseBuffer += decoder.decode(chunk, { stream: true });
|
|
116
125
|
const lines = sseBuffer.split('\n');
|
|
117
|
-
sseBuffer = lines.pop();
|
|
126
|
+
sseBuffer = lines.pop();
|
|
118
127
|
|
|
119
128
|
for (const line of lines) {
|
|
120
129
|
const trimmed = line.replace(/\r$/, '');
|
|
@@ -124,15 +133,12 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
124
133
|
|
|
125
134
|
if (ev.status_delta !== undefined) {
|
|
126
135
|
printStatus(ev.status_delta);
|
|
127
|
-
|
|
128
136
|
} else if (ev.reason_delta !== undefined) {
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
reasonBuf += ev.reason_delta;
|
|
131
138
|
} else if (ev.delta !== undefined) {
|
|
132
139
|
rawAnswer += ev.delta;
|
|
133
140
|
const txt = parseStreamText(rawAnswer);
|
|
134
141
|
if (txt !== null) answerBuf = txt;
|
|
135
|
-
|
|
136
142
|
} else if (ev.done) {
|
|
137
143
|
finalResult = ev.result;
|
|
138
144
|
}
|
|
@@ -140,42 +146,72 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
// ── Final render ─────────────────────────────────────────────────────────
|
|
143
|
-
|
|
149
|
+
stopSpinner();
|
|
144
150
|
flushReasoning();
|
|
145
151
|
|
|
146
|
-
const reply
|
|
152
|
+
const reply = finalResult?.reply ?? {};
|
|
147
153
|
const replyType = reply.type ?? 'message';
|
|
148
154
|
|
|
155
|
+
process.stdout.write('\n' + chalk.dim(' ' + '─'.repeat(58)) + '\n\n');
|
|
156
|
+
|
|
149
157
|
if (replyType === 'message') {
|
|
150
158
|
const text = reply.text || answerBuf || '(no response)';
|
|
151
|
-
|
|
152
|
-
|
|
159
|
+
const rendered = renderMarkdown(text);
|
|
160
|
+
const indented = rendered.split('\n').map(l => ' ' + l).join('\n');
|
|
161
|
+
process.stdout.write(indented + '\n\n');
|
|
153
162
|
return text;
|
|
154
163
|
|
|
155
164
|
} else if (replyType === 'plan') {
|
|
156
|
-
process.stdout.write('\n');
|
|
157
|
-
process.stdout.write(chalk.bold.cyan('📋 Plan: ') + chalk.cyan(reply.summary || '') + '\n');
|
|
165
|
+
process.stdout.write(' ' + chalk.bold.cyan('📋 ') + chalk.bold.white(reply.summary || 'Plan') + '\n\n');
|
|
158
166
|
(reply.steps || []).forEach((step, i) => {
|
|
159
|
-
const risk = step.risk === 'high' ? chalk.red('
|
|
160
|
-
process.stdout.write(
|
|
161
|
-
|
|
162
|
-
);
|
|
163
|
-
if (step.purpose) process.stdout.write(chalk.dim(` ${step.purpose}\n`));
|
|
167
|
+
const risk = step.risk === 'high' ? chalk.red(' ⚠ high risk') : '';
|
|
168
|
+
process.stdout.write(` ${chalk.cyan(String(i + 1) + '.')} ${chalk.yellow(step.cmd || '')}${risk}\n`);
|
|
169
|
+
if (step.purpose) process.stdout.write(chalk.dim(` ${step.purpose}\n`));
|
|
164
170
|
});
|
|
165
171
|
process.stdout.write('\n');
|
|
166
172
|
return reply.summary || '';
|
|
167
173
|
|
|
168
174
|
} else if (replyType === 'connect') {
|
|
169
175
|
const dev = reply.device ?? {};
|
|
170
|
-
process.stdout.write('\n');
|
|
171
176
|
process.stdout.write(
|
|
172
|
-
chalk.bold.green('🔌
|
|
173
|
-
chalk.green(`${dev.kind ?? '?'}#${dev.id ?? '?'}
|
|
177
|
+
' ' + chalk.bold.green('🔌 ') +
|
|
178
|
+
chalk.bold.green(`${dev.kind ?? '?'}#${dev.id ?? '?'}`) +
|
|
179
|
+
chalk.white(` ${dev.name ?? ''} `) +
|
|
180
|
+
chalk.dim(`(${dev.host ?? ''})`) + '\n'
|
|
174
181
|
);
|
|
175
|
-
if (reply.note) process.stdout.write(chalk.dim('
|
|
182
|
+
if (reply.note) process.stdout.write(chalk.dim(' ' + reply.note) + '\n');
|
|
176
183
|
process.stdout.write('\n');
|
|
177
184
|
return reply.note || '';
|
|
178
185
|
}
|
|
179
186
|
|
|
180
187
|
return answerBuf;
|
|
181
188
|
}
|
|
189
|
+
|
|
190
|
+
/** Print a styled welcome banner. */
|
|
191
|
+
export function printBanner(cfg) {
|
|
192
|
+
const line = '─'.repeat(58);
|
|
193
|
+
process.stdout.write('\n');
|
|
194
|
+
process.stdout.write(chalk.cyan(' ╭' + line + '╮') + '\n');
|
|
195
|
+
process.stdout.write(
|
|
196
|
+
chalk.cyan(' │ ') +
|
|
197
|
+
chalk.bold.white('iCopilot') +
|
|
198
|
+
chalk.dim(' · IspBills AI Network Engineer') +
|
|
199
|
+
' '.repeat(13) +
|
|
200
|
+
chalk.cyan(' │') + '\n'
|
|
201
|
+
);
|
|
202
|
+
process.stdout.write(chalk.cyan(' ╰' + line + '╯') + '\n\n');
|
|
203
|
+
|
|
204
|
+
if (cfg?.url) {
|
|
205
|
+
process.stdout.write(chalk.dim(' Connected ') + chalk.cyan(cfg.url) + '\n');
|
|
206
|
+
}
|
|
207
|
+
if (cfg?.user?.name || cfg?.user?.email) {
|
|
208
|
+
process.stdout.write(
|
|
209
|
+
chalk.dim(' Logged in ') +
|
|
210
|
+
chalk.white(cfg.user.name || cfg.user.email) +
|
|
211
|
+
chalk.dim(' · ' + (cfg.user.role ?? 'operator')) + '\n'
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
process.stdout.write('\n');
|
|
215
|
+
process.stdout.write(chalk.dim(' Type your question and press Enter. Type "exit" to quit.\n'));
|
|
216
|
+
process.stdout.write(chalk.dim(' ' + line) + '\n\n');
|
|
217
|
+
}
|