ispbills-icli 4.0.5 → 5.2.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 +180 -19
- package/package.json +1 -1
- package/src/chat.js +79 -37
- package/src/config.js +11 -1
- package/src/layout.js +55 -0
package/bin/icli.js
CHANGED
|
@@ -1,15 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createInterface } from 'readline';
|
|
3
|
-
import { stdin as input, stdout as output, argv, exit } from 'process';
|
|
2
|
+
import { createInterface, emitKeypressEvents } from 'readline';
|
|
3
|
+
import { stdin as input, stdout as output, argv, exit, platform } from 'process';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
4
8
|
import chalk from 'chalk';
|
|
5
|
-
import { loadConfig, configPath } from '../src/config.js';
|
|
9
|
+
import { loadConfig, configPath, clearConfig } from '../src/config.js';
|
|
6
10
|
import { loginFlow } from '../src/auth.js';
|
|
7
|
-
import { inner, onResize, boxTop, boxBot, boxMid, boxRow } from '../src/layout.js';
|
|
11
|
+
import { inner, onResize, boxTop, boxBot, boxMid, boxRow, bottomPalette } from '../src/layout.js';
|
|
8
12
|
import {
|
|
9
13
|
streamChat, printBanner, printSlashMenu, printUserBox,
|
|
10
14
|
SLASH_COMMANDS, slashCompleter,
|
|
11
15
|
} from '../src/chat.js';
|
|
12
16
|
|
|
17
|
+
const PKG_NAME = 'ispbills-icli';
|
|
18
|
+
|
|
19
|
+
/** Read this CLI's installed version from its package.json. */
|
|
20
|
+
function pkgVersion() {
|
|
21
|
+
try {
|
|
22
|
+
const p = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
23
|
+
return JSON.parse(readFileSync(p, 'utf8')).version ?? 'unknown';
|
|
24
|
+
} catch {
|
|
25
|
+
return 'unknown';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Self-update: install the latest published version globally via npm.
|
|
31
|
+
* Returns true on success. The running process keeps the old code until the
|
|
32
|
+
* next launch, so callers should tell the user to restart.
|
|
33
|
+
*/
|
|
34
|
+
function selfUpdate(P) {
|
|
35
|
+
const cur = pkgVersion();
|
|
36
|
+
process.stdout.write('\n ' + P.dim('Current version: ') + chalk.white(cur) + '\n');
|
|
37
|
+
process.stdout.write(' ' + P.muted('Updating ' + PKG_NAME + ' to the latest version…') + '\n\n');
|
|
38
|
+
|
|
39
|
+
const npm = platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
40
|
+
const res = spawnSync(npm, ['install', '-g', `${PKG_NAME}@latest`], {
|
|
41
|
+
stdio: 'inherit',
|
|
42
|
+
shell: platform === 'win32',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (res.status === 0) {
|
|
46
|
+
process.stdout.write('\n ' + P.ok('✓') + ' iCli updated. ' +
|
|
47
|
+
P.dim('Restart iCli to use the new version.') + '\n\n');
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
process.stdout.write('\n ' + P.err('✖') + ' Update failed. Try manually:\n');
|
|
51
|
+
process.stdout.write(' ' + P.accent(`npm install -g ${PKG_NAME}@latest`) + '\n');
|
|
52
|
+
process.stdout.write(' ' + P.dim('On some systems this needs elevated privileges (sudo / Administrator).') + '\n\n');
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
13
56
|
const P = {
|
|
14
57
|
border: chalk.hex('#303134'),
|
|
15
58
|
dim: chalk.hex('#9AA0A6'),
|
|
@@ -25,6 +68,13 @@ const P = {
|
|
|
25
68
|
const args = argv.slice(2);
|
|
26
69
|
const cmd = args[0] ?? '';
|
|
27
70
|
|
|
71
|
+
// ── Session context (shown in top bar) ─────────────────────────────────────────
|
|
72
|
+
const ctx = {
|
|
73
|
+
device: '',
|
|
74
|
+
aiMode: 'MANUAL', // 'AUTOPILOT ON' | 'MANUAL'
|
|
75
|
+
operator: '',
|
|
76
|
+
};
|
|
77
|
+
|
|
28
78
|
// ── Help ───────────────────────────────────────────────────────────────────────
|
|
29
79
|
function printHelp() {
|
|
30
80
|
process.stdout.write('\n');
|
|
@@ -37,6 +87,8 @@ function printHelp() {
|
|
|
37
87
|
['icli', 'Interactive REPL with slash commands'],
|
|
38
88
|
['icli "question"', 'Single-shot query then exit'],
|
|
39
89
|
['icli login', 'Authenticate to your IspBills instance'],
|
|
90
|
+
['icli logout', 'Log out and clear saved credentials'],
|
|
91
|
+
['icli update', 'Update iCli to the latest version'],
|
|
40
92
|
['icli whoami', 'Show current session info'],
|
|
41
93
|
['icli --help', 'Show this help'],
|
|
42
94
|
].forEach(([c, d]) =>
|
|
@@ -52,19 +104,23 @@ function printHelp() {
|
|
|
52
104
|
}
|
|
53
105
|
|
|
54
106
|
process.stdout.write(boxMid(P.border) + '\n');
|
|
55
|
-
process.stdout.write(boxRow(P.bold('
|
|
107
|
+
process.stdout.write(boxRow(P.bold('Function key shortcuts'), P.border) + '\n');
|
|
56
108
|
process.stdout.write(boxRow('', P.border) + '\n');
|
|
57
109
|
[
|
|
58
|
-
['
|
|
59
|
-
['
|
|
60
|
-
['
|
|
61
|
-
['
|
|
62
|
-
['
|
|
110
|
+
['F1', 'Open documentation'],
|
|
111
|
+
['F2', 'Show conversation history'],
|
|
112
|
+
['F3', 'Rollback last AI action'],
|
|
113
|
+
['F4', 'Run bulk device sweep'],
|
|
114
|
+
['Tab', 'Autocomplete slash commands'],
|
|
115
|
+
['↑ / ↓', 'Navigate input history'],
|
|
116
|
+
['Ctrl+L', 'Clear screen'],
|
|
117
|
+
['Ctrl+C', 'Cancel / exit'],
|
|
63
118
|
].forEach(([k, d]) =>
|
|
64
119
|
process.stdout.write(boxRow(P.warn(k.padEnd(14)) + P.dim(d), P.border) + '\n')
|
|
65
120
|
);
|
|
66
121
|
process.stdout.write(boxRow('', P.border) + '\n');
|
|
67
|
-
process.stdout.write(boxBot(P.border) + '\n
|
|
122
|
+
process.stdout.write(boxBot(P.border) + '\n');
|
|
123
|
+
process.stdout.write(bottomPalette() + '\n\n');
|
|
68
124
|
}
|
|
69
125
|
|
|
70
126
|
// ── Auth ───────────────────────────────────────────────────────────────────────
|
|
@@ -108,11 +164,16 @@ async function singleShot(cfg, question) {
|
|
|
108
164
|
|
|
109
165
|
// ── REPL ───────────────────────────────────────────────────────────────────────
|
|
110
166
|
async function interactiveRepl(cfg) {
|
|
111
|
-
const history
|
|
112
|
-
|
|
113
|
-
const PROMPT = () => ' ' + P.accentD('❯') + ' ';
|
|
167
|
+
const history = [];
|
|
168
|
+
const PROMPT = () => ' ' + P.accentD('❯') + ' ';
|
|
114
169
|
|
|
115
|
-
|
|
170
|
+
let isProcessing = false;
|
|
171
|
+
|
|
172
|
+
// Populate operator from cfg
|
|
173
|
+
if (cfg.user?.name) ctx.operator = cfg.user.name;
|
|
174
|
+
if (cfg.user?.role) ctx.aiMode = cfg.user.role === 'noc' ? 'AUTOPILOT ON' : 'MANUAL';
|
|
175
|
+
|
|
176
|
+
printBanner(cfg, {}, ctx);
|
|
116
177
|
|
|
117
178
|
const rl = createInterface({
|
|
118
179
|
input, output,
|
|
@@ -122,6 +183,74 @@ async function interactiveRepl(cfg) {
|
|
|
122
183
|
});
|
|
123
184
|
|
|
124
185
|
onResize(() => rl.setPrompt(PROMPT()));
|
|
186
|
+
|
|
187
|
+
// ── F-key handling via keypress events ───────────────────────────────────────
|
|
188
|
+
emitKeypressEvents(process.stdin);
|
|
189
|
+
|
|
190
|
+
process.stdin.on('keypress', async (_ch, key) => {
|
|
191
|
+
if (!key || rl.closed || isProcessing) return;
|
|
192
|
+
|
|
193
|
+
if (key.name === 'f1') {
|
|
194
|
+
process.stdout.write('\n');
|
|
195
|
+
printHelp();
|
|
196
|
+
rl.prompt();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (key.name === 'f2') {
|
|
201
|
+
// Show history
|
|
202
|
+
const turns = history.filter(m => m.role === 'user');
|
|
203
|
+
process.stdout.write('\n');
|
|
204
|
+
if (!turns.length) {
|
|
205
|
+
process.stdout.write(P.muted(' No history yet.\n\n'));
|
|
206
|
+
} else {
|
|
207
|
+
process.stdout.write(boxTop('History', P.border) + '\n');
|
|
208
|
+
turns.forEach((m, i) =>
|
|
209
|
+
process.stdout.write(
|
|
210
|
+
boxRow(P.dim(String(i + 1).padStart(2) + '.') + ' ' +
|
|
211
|
+
chalk.white(m.content.replace(/\s+/g, ' ').slice(0, inner() - 5)), P.border) + '\n')
|
|
212
|
+
);
|
|
213
|
+
process.stdout.write(boxBot(P.border) + '\n\n');
|
|
214
|
+
}
|
|
215
|
+
rl.prompt();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (key.name === 'f3') {
|
|
220
|
+
rl.pause();
|
|
221
|
+
const q = 'rollback the last change you made';
|
|
222
|
+
process.stdout.write('\n ' + P.muted('→ ') + P.dim(q) + '\n');
|
|
223
|
+
printUserBox(q);
|
|
224
|
+
history.push({ role: 'user', content: q });
|
|
225
|
+
try {
|
|
226
|
+
const answer = await streamChat(cfg, [...history]);
|
|
227
|
+
if (answer) history.push({ role: 'assistant', content: answer });
|
|
228
|
+
} catch (e) {
|
|
229
|
+
process.stdout.write('\n ' + P.err('✖ ') + chalk.white(e.message) + '\n\n');
|
|
230
|
+
}
|
|
231
|
+
rl.resume();
|
|
232
|
+
rl.prompt();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (key.name === 'f4') {
|
|
237
|
+
rl.pause();
|
|
238
|
+
const q = 'run a bulk sweep of all devices and report status';
|
|
239
|
+
process.stdout.write('\n ' + P.muted('→ ') + P.dim(q) + '\n');
|
|
240
|
+
printUserBox(q);
|
|
241
|
+
history.push({ role: 'user', content: q });
|
|
242
|
+
try {
|
|
243
|
+
const answer = await streamChat(cfg, [...history]);
|
|
244
|
+
if (answer) history.push({ role: 'assistant', content: answer });
|
|
245
|
+
} catch (e) {
|
|
246
|
+
process.stdout.write('\n ' + P.err('✖ ') + chalk.white(e.message) + '\n\n');
|
|
247
|
+
}
|
|
248
|
+
rl.resume();
|
|
249
|
+
rl.prompt();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
125
254
|
rl.prompt();
|
|
126
255
|
|
|
127
256
|
rl.on('line', async (line) => {
|
|
@@ -132,7 +261,25 @@ async function interactiveRepl(cfg) {
|
|
|
132
261
|
process.stdout.write(P.muted('\n Goodbye.\n\n')); rl.close(); exit(0);
|
|
133
262
|
}
|
|
134
263
|
if (q === '/clear' || q === 'clear') {
|
|
135
|
-
process.stdout.write('\x1Bc');
|
|
264
|
+
process.stdout.write('\x1Bc');
|
|
265
|
+
printBanner(cfg, {}, ctx);
|
|
266
|
+
rl.prompt();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (q === '/update' || q === 'update') {
|
|
270
|
+
rl.pause();
|
|
271
|
+
selfUpdate(P);
|
|
272
|
+
rl.resume();
|
|
273
|
+
rl.prompt();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (q === '/logout' || q === 'logout') {
|
|
277
|
+
const cleared = clearConfig();
|
|
278
|
+
process.stdout.write('\n ' + (cleared ? P.ok('✓') + ' Logged out — credentials cleared.'
|
|
279
|
+
: P.warn('Already logged out.')) +
|
|
280
|
+
'\n ' + P.dim('Run ') + P.accent('icli login') + P.dim(' to sign in again.') + '\n\n');
|
|
281
|
+
rl.close();
|
|
282
|
+
exit(0);
|
|
136
283
|
}
|
|
137
284
|
if (q === '/help' || q === 'help') { printHelp(); rl.prompt(); return; }
|
|
138
285
|
if (q === '/history' || q === 'history') {
|
|
@@ -162,12 +309,10 @@ async function interactiveRepl(cfg) {
|
|
|
162
309
|
|
|
163
310
|
rl.pause();
|
|
164
311
|
history.push({ role: 'user', content: question });
|
|
165
|
-
|
|
166
|
-
// User message box
|
|
167
312
|
printUserBox(question);
|
|
168
313
|
|
|
169
314
|
try {
|
|
170
|
-
const answer = await streamChat(cfg, [...history]
|
|
315
|
+
const answer = await streamChat(cfg, [...history]);
|
|
171
316
|
if (answer) history.push({ role: 'assistant', content: answer });
|
|
172
317
|
} catch (e) {
|
|
173
318
|
process.stdout.write('\n ' + P.err('✖ ') + chalk.white(e.message) + '\n\n');
|
|
@@ -193,6 +338,22 @@ async function main() {
|
|
|
193
338
|
return;
|
|
194
339
|
}
|
|
195
340
|
|
|
341
|
+
if (cmd === 'update') {
|
|
342
|
+
const ok = selfUpdate(P);
|
|
343
|
+
exit(ok ? 0 : 1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (cmd === 'logout') {
|
|
347
|
+
const cleared = clearConfig();
|
|
348
|
+
if (cleared) {
|
|
349
|
+
process.stdout.write('\n ' + P.ok('✓') + ' Logged out — credentials cleared.\n');
|
|
350
|
+
process.stdout.write(' ' + P.dim('Run ') + P.accent('icli login') + P.dim(' to sign in again.') + '\n\n');
|
|
351
|
+
} else {
|
|
352
|
+
process.stdout.write('\n ' + P.warn('Not logged in.') + ' Nothing to clear.\n\n');
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
196
357
|
if (cmd === 'whoami') {
|
|
197
358
|
const cfg = loadConfig();
|
|
198
359
|
if (!cfg) {
|
package/package.json
CHANGED
package/src/chat.js
CHANGED
|
@@ -5,7 +5,7 @@ import TerminalRenderer from 'marked-terminal';
|
|
|
5
5
|
import { refreshToken } from './auth.js';
|
|
6
6
|
import { cols, inner, visLen, padRight, stripAnsi,
|
|
7
7
|
boxTop, boxMid, boxBot, boxTopOpen, boxBotOpen,
|
|
8
|
-
boxRow, boxLine } from './layout.js';
|
|
8
|
+
boxRow, boxLine, bottomPalette, topBar } from './layout.js';
|
|
9
9
|
|
|
10
10
|
// ── Gemini-style palette ───────────────────────────────────────────────────────
|
|
11
11
|
const P = {
|
|
@@ -21,6 +21,7 @@ const P = {
|
|
|
21
21
|
warn: chalk.yellow,
|
|
22
22
|
cyan: chalk.cyan,
|
|
23
23
|
reasoning: chalk.hex('#9198A1').italic,
|
|
24
|
+
pill: chalk.bgHex('#1a2b47').hex('#8AB4F8'),
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
// ── Markdown ───────────────────────────────────────────────────────────────────
|
|
@@ -36,11 +37,9 @@ const termRenderer = new TerminalRenderer({
|
|
|
36
37
|
link: (_h, _t, t) => chalk.hex('#58a6ff').underline(t || _h),
|
|
37
38
|
});
|
|
38
39
|
|
|
39
|
-
// Override listitem on the instance — the options.listitem is a chalk color, not a renderer fn
|
|
40
40
|
const _origListitem = termRenderer.listitem.bind(termRenderer);
|
|
41
41
|
termRenderer.listitem = function(item) {
|
|
42
42
|
const raw = _origListitem(item);
|
|
43
|
-
// Replace the default ' * ' or ' - ' bullet with our accent '·'
|
|
44
43
|
return raw.replace(/^\s*[*\-]\s/, (m) => m.replace(/[*\-]/, P.accent('·')));
|
|
45
44
|
};
|
|
46
45
|
|
|
@@ -68,13 +67,21 @@ const shortModel = (m) =>
|
|
|
68
67
|
m.replace('openai/', '').replace(/^claude-/, '').replace(/^gpt-/, 'gpt-');
|
|
69
68
|
|
|
70
69
|
// ── Banner ────────────────────────────────────────────────────────────────────
|
|
71
|
-
export function printBanner(cfg, meta = {}) {
|
|
70
|
+
export function printBanner(cfg, meta = {}, ctx = {}) {
|
|
72
71
|
const model = meta.model ? shortModel(meta.model) : (cfg?._model ?? null);
|
|
73
72
|
|
|
74
73
|
process.stdout.write('\n');
|
|
75
74
|
process.stdout.write(boxTop('iCopilot', P.border) + '\n');
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
|
|
76
|
+
// Context bar (device · aiMode · operator) — delegates to topBar() in layout.js
|
|
77
|
+
const { device = '', aiMode = 'MANUAL', operator = '' } = ctx;
|
|
78
|
+
if (device || aiMode !== 'MANUAL' || operator) {
|
|
79
|
+
process.stdout.write(topBar(ctx) + '\n');
|
|
80
|
+
process.stdout.write(boxMid(P.border) + '\n');
|
|
81
|
+
} else {
|
|
82
|
+
process.stdout.write(boxRow(P.dim('Gemini-style terminal · iCopilot AI'), P.border) + '\n');
|
|
83
|
+
process.stdout.write(boxMid(P.border) + '\n');
|
|
84
|
+
}
|
|
78
85
|
|
|
79
86
|
const row1parts = [];
|
|
80
87
|
if (model) row1parts.push(P.dim('model ') + P.white(model));
|
|
@@ -98,7 +105,8 @@ export function printBanner(cfg, meta = {}) {
|
|
|
98
105
|
P.accentB('Ctrl+C') + P.muted(' exit'),
|
|
99
106
|
].join(P.muted(' '));
|
|
100
107
|
process.stdout.write(boxRow(hints, P.border) + '\n');
|
|
101
|
-
process.stdout.write(boxBot(P.border) + '\n
|
|
108
|
+
process.stdout.write(boxBot(P.border) + '\n');
|
|
109
|
+
process.stdout.write(bottomPalette() + '\n\n');
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
// ── Slash commands ─────────────────────────────────────────────────────────────
|
|
@@ -111,6 +119,8 @@ export const SLASH_COMMANDS = [
|
|
|
111
119
|
{ cmd: '/online', hint: '', desc: 'List online sessions' },
|
|
112
120
|
{ cmd: '/history', hint: '', desc: 'Show conversation history' },
|
|
113
121
|
{ cmd: '/clear', hint: '', desc: 'Clear the screen' },
|
|
122
|
+
{ cmd: '/update', hint: '', desc: 'Update iCli to the latest version'},
|
|
123
|
+
{ cmd: '/logout', hint: '', desc: 'Log out and clear credentials'},
|
|
114
124
|
{ cmd: '/help', hint: '', desc: 'Show help' },
|
|
115
125
|
{ cmd: '/exit', hint: '', desc: 'Exit iCli' },
|
|
116
126
|
];
|
|
@@ -138,7 +148,6 @@ export function slashCompleter(line) {
|
|
|
138
148
|
export function printUserBox(question) {
|
|
139
149
|
process.stdout.write('\n');
|
|
140
150
|
process.stdout.write(boxTop('You', P.border) + '\n');
|
|
141
|
-
// Word-wrap the question to fit inside the box
|
|
142
151
|
const maxW = inner();
|
|
143
152
|
const words = question.split(' ');
|
|
144
153
|
let line = '';
|
|
@@ -155,8 +164,14 @@ export function printUserBox(question) {
|
|
|
155
164
|
}
|
|
156
165
|
|
|
157
166
|
// ── streamChat ────────────────────────────────────────────────────────────────
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Stream a chat request. Reasoning and plan always flow inline — no keys to toggle.
|
|
169
|
+
*
|
|
170
|
+
* @param {object} cfg Loaded config (url, token, …)
|
|
171
|
+
* @param {Array} messages Conversation history
|
|
172
|
+
* @param {object} [context] Extra context passed to server
|
|
173
|
+
*/
|
|
174
|
+
export async function streamChat(cfg, messages, context = {}) {
|
|
160
175
|
const body = JSON.stringify({ messages, context });
|
|
161
176
|
|
|
162
177
|
async function doFetch(token) {
|
|
@@ -165,7 +180,7 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
165
180
|
headers: {
|
|
166
181
|
'Content-Type': 'application/json',
|
|
167
182
|
'Accept': 'text/event-stream',
|
|
168
|
-
'Authorization':
|
|
183
|
+
'Authorization': 'Bearer ' + token,
|
|
169
184
|
},
|
|
170
185
|
body,
|
|
171
186
|
});
|
|
@@ -184,7 +199,7 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
184
199
|
}
|
|
185
200
|
|
|
186
201
|
// ── Streaming state ─────────────────────────────────────────────────────────
|
|
187
|
-
let boxOpen = false;
|
|
202
|
+
let boxOpen = false;
|
|
188
203
|
let reasoningOpen = false;
|
|
189
204
|
let answerBuf = '';
|
|
190
205
|
let rawAnswer = '';
|
|
@@ -196,38 +211,42 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
196
211
|
process.stdout.write('\n' + boxTopOpen('iCopilot', P.border) + '\n');
|
|
197
212
|
}
|
|
198
213
|
|
|
214
|
+
// Gemini-style thinking pill spinner
|
|
199
215
|
const spinner = ora({
|
|
200
|
-
text: P.
|
|
216
|
+
text: P.accent('Thinking…'),
|
|
201
217
|
color: 'blue',
|
|
202
|
-
spinner: '
|
|
203
|
-
prefixText: P.border('│') + ' ',
|
|
218
|
+
spinner: { interval: 80, frames: ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'] },
|
|
219
|
+
prefixText: P.border('│') + ' ' + P.pill(' ◆ '),
|
|
204
220
|
});
|
|
205
221
|
|
|
206
222
|
const stopSpinner = () => { if (spinner.isSpinning) spinner.stop(); };
|
|
207
223
|
|
|
224
|
+
// ── Reasoning section (always streams inline) ───────────────────────────────
|
|
208
225
|
function openReasoning() {
|
|
209
226
|
if (reasoningOpen) return;
|
|
210
227
|
stopSpinner();
|
|
211
228
|
reasoningOpen = true;
|
|
212
|
-
process.stdout.write(boxLine(
|
|
213
|
-
process.stdout.write(boxLine(P.
|
|
229
|
+
process.stdout.write(boxLine('', P.border) + '\n');
|
|
230
|
+
process.stdout.write(boxLine(P.accentB('◆ ') + P.dim('Reasoning'), P.border) + '\n');
|
|
231
|
+
process.stdout.write(boxLine(P.border(' │ '), P.border));
|
|
214
232
|
}
|
|
215
233
|
|
|
216
234
|
function writeReasonDelta(delta) {
|
|
217
|
-
if (!reasoningOpen)
|
|
235
|
+
if (!reasoningOpen) openReasoning();
|
|
218
236
|
const parts = delta.split('\n');
|
|
219
237
|
for (let i = 0; i < parts.length; i++) {
|
|
220
|
-
if (i > 0) process.stdout.write('\n' + boxLine(P.border('│
|
|
238
|
+
if (i > 0) process.stdout.write('\n' + boxLine(P.border(' │'), P.border));
|
|
221
239
|
if (parts[i]) process.stdout.write(P.reasoning(parts[i]));
|
|
222
240
|
}
|
|
223
241
|
}
|
|
224
242
|
|
|
225
243
|
function closeReasoning() {
|
|
226
244
|
if (!reasoningOpen) return;
|
|
227
|
-
process.stdout.write('\n');
|
|
245
|
+
process.stdout.write('\n' + boxLine('', P.border) + '\n');
|
|
228
246
|
reasoningOpen = false;
|
|
229
247
|
}
|
|
230
248
|
|
|
249
|
+
// ── Status delta ────────────────────────────────────────────────────────────
|
|
231
250
|
function printStatus(line) {
|
|
232
251
|
ensureBoxOpen();
|
|
233
252
|
if (reasoningOpen) closeReasoning();
|
|
@@ -236,7 +255,8 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
236
255
|
const text = line.trimStart();
|
|
237
256
|
const indent = /^[\s·•⌁]/.test(line) ? ' ' : '';
|
|
238
257
|
process.stdout.write(boxLine(indent + icon + ' ' + P.dim(text), P.border) + '\n');
|
|
239
|
-
|
|
258
|
+
// Restart spinner after each status line
|
|
259
|
+
spinner.start();
|
|
240
260
|
}
|
|
241
261
|
|
|
242
262
|
function parseStreamText(raw) {
|
|
@@ -247,7 +267,8 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
247
267
|
}
|
|
248
268
|
|
|
249
269
|
// ── SSE reader ─────────────────────────────────────────────────────────────
|
|
250
|
-
|
|
270
|
+
ensureBoxOpen();
|
|
271
|
+
spinner.start();
|
|
251
272
|
|
|
252
273
|
const decoder = new TextDecoder();
|
|
253
274
|
let sseBuffer = '';
|
|
@@ -266,7 +287,6 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
266
287
|
printStatus(ev.status_delta);
|
|
267
288
|
} else if (ev.reason_delta !== undefined) {
|
|
268
289
|
ensureBoxOpen();
|
|
269
|
-
if (!reasoningOpen) openReasoning();
|
|
270
290
|
writeReasonDelta(ev.reason_delta);
|
|
271
291
|
} else if (ev.delta !== undefined) {
|
|
272
292
|
if (reasoningOpen) closeReasoning();
|
|
@@ -291,7 +311,6 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
291
311
|
|
|
292
312
|
if (meta.model && cfg) cfg._model = shortModel(meta.model);
|
|
293
313
|
|
|
294
|
-
// Blank separator before content
|
|
295
314
|
process.stdout.write(boxLine('', P.border) + '\n');
|
|
296
315
|
|
|
297
316
|
if (replyType === 'message') {
|
|
@@ -302,36 +321,59 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
302
321
|
}
|
|
303
322
|
|
|
304
323
|
} else if (replyType === 'plan') {
|
|
305
|
-
|
|
306
|
-
|
|
324
|
+
// Plan always rendered inline — no toggle key
|
|
325
|
+
process.stdout.write(boxLine(P.accentB('◆ ') + P.bold('Plan'), P.border) + '\n');
|
|
326
|
+
if (reply.summary) {
|
|
327
|
+
process.stdout.write(boxLine(P.dim(reply.summary), P.border) + '\n');
|
|
328
|
+
}
|
|
307
329
|
process.stdout.write(boxLine('', P.border) + '\n');
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
330
|
+
|
|
331
|
+
const steps = reply.steps || [];
|
|
332
|
+
steps.forEach((s, i) => {
|
|
333
|
+
const isLast = i === steps.length - 1;
|
|
334
|
+
const prefix = isLast ? '└─ ' : '├─ ';
|
|
335
|
+
const risk = s.risk === 'high' ? ' ' + P.err('⚠ high risk') :
|
|
336
|
+
s.risk === 'medium' ? ' ' + P.warn('⚡') : '';
|
|
337
|
+
process.stdout.write(
|
|
338
|
+
boxLine(
|
|
339
|
+
P.border(' ' + prefix) + P.dim(String(i + 1) + '.') + ' ' + P.warn(s.cmd || '') + risk,
|
|
340
|
+
P.border,
|
|
341
|
+
) + '\n',
|
|
342
|
+
);
|
|
343
|
+
if (s.purpose) {
|
|
344
|
+
process.stdout.write(boxLine(' ' + P.muted(s.purpose), P.border) + '\n');
|
|
345
|
+
}
|
|
313
346
|
});
|
|
347
|
+
|
|
314
348
|
process.stdout.write(boxLine('', P.border) + '\n');
|
|
315
|
-
process.stdout.write(
|
|
349
|
+
process.stdout.write(
|
|
350
|
+
boxLine(P.dim('Type ') + P.white('apply fix') + P.dim(' to confirm.'), P.border) + '\n',
|
|
351
|
+
);
|
|
316
352
|
|
|
317
353
|
} else if (replyType === 'connect') {
|
|
318
354
|
const dev = reply.device ?? {};
|
|
319
355
|
process.stdout.write(
|
|
320
|
-
boxLine(
|
|
321
|
-
' ' + P.
|
|
356
|
+
boxLine(
|
|
357
|
+
P.ok('🔌 ') + P.bold(`${dev.kind ?? '?'}#${dev.id ?? '?'}`) +
|
|
358
|
+
' ' + P.white(dev.name ?? '') + ' ' + P.dim(`(${dev.host ?? ''})`),
|
|
359
|
+
P.border,
|
|
360
|
+
) + '\n',
|
|
322
361
|
);
|
|
323
362
|
if (reply.note) process.stdout.write(boxLine(P.dim(reply.note), P.border) + '\n');
|
|
324
363
|
}
|
|
325
364
|
|
|
326
|
-
//
|
|
365
|
+
// Footer inside box
|
|
327
366
|
process.stdout.write(boxLine('', P.border) + '\n');
|
|
328
367
|
const foot = [meta.model ? shortModel(meta.model) : null, meta.user, meta.role].filter(Boolean);
|
|
329
368
|
if (foot.length) {
|
|
330
369
|
process.stdout.write(boxLine(P.muted(foot.join(' · ')), P.border) + '\n');
|
|
331
370
|
}
|
|
332
371
|
|
|
333
|
-
// Close the box
|
|
334
|
-
process.stdout.write(boxBotOpen(P.border) + '\n
|
|
372
|
+
// Close the open box
|
|
373
|
+
process.stdout.write(boxBotOpen(P.border) + '\n');
|
|
374
|
+
|
|
375
|
+
// Bottom palette after every AI reply
|
|
376
|
+
process.stdout.write(bottomPalette() + '\n\n');
|
|
335
377
|
|
|
336
378
|
return reply.text || answerBuf || '';
|
|
337
379
|
}
|
package/src/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'fs';
|
|
2
2
|
import { homedir } from 'os';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
|
|
@@ -21,3 +21,13 @@ export function saveConfig(cfg) {
|
|
|
21
21
|
export function configPath() {
|
|
22
22
|
return CONFIG_FILE;
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Remove the saved credentials (log out). Returns true if a config file was
|
|
27
|
+
* present and deleted, false if there was nothing to clear.
|
|
28
|
+
*/
|
|
29
|
+
export function clearConfig() {
|
|
30
|
+
if (!existsSync(CONFIG_FILE)) return false;
|
|
31
|
+
rmSync(CONFIG_FILE, { force: true });
|
|
32
|
+
return true;
|
|
33
|
+
}
|
package/src/layout.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
|
|
3
4
|
// ── Terminal width ─────────────────────────────────────────────────────────────
|
|
4
5
|
let cachedCols = 0;
|
|
@@ -152,3 +153,57 @@ export function boxRow(content, borderFn) {
|
|
|
152
153
|
export function boxLine(content, borderFn) {
|
|
153
154
|
return borderFn('│') + ' ' + content;
|
|
154
155
|
}
|
|
156
|
+
|
|
157
|
+
// ── Top context bar ───────────────────────────────────────────────────────────
|
|
158
|
+
/**
|
|
159
|
+
* Renders a single full-width bar:
|
|
160
|
+
* │ ● device · host ◆ AUTOPILOT ON operator: admin │
|
|
161
|
+
*
|
|
162
|
+
* @param {{ device?: string, aiMode?: 'AUTOPILOT ON'|'MANUAL', operator?: string }} ctx
|
|
163
|
+
*/
|
|
164
|
+
export function topBar({ device = '', aiMode = 'MANUAL', operator = '' } = {}) {
|
|
165
|
+
const w = cols();
|
|
166
|
+
|
|
167
|
+
const lRaw = device ? `● ${device}` : '';
|
|
168
|
+
const cRaw = aiMode === 'AUTOPILOT ON' ? '◆ AUTOPILOT ON' : '◇ AI MANUAL';
|
|
169
|
+
const rRaw = operator ? `operator: ${operator}` : '';
|
|
170
|
+
|
|
171
|
+
const lC = lRaw ? chalk.hex('#8AB4F8').bold(lRaw) : '';
|
|
172
|
+
const cC = aiMode === 'AUTOPILOT ON'
|
|
173
|
+
? chalk.green.bold(cRaw)
|
|
174
|
+
: chalk.yellow(cRaw);
|
|
175
|
+
const rC = rRaw ? chalk.hex('#9AA0A6')(rRaw) : '';
|
|
176
|
+
|
|
177
|
+
// avail = w - 2 borders - 2 side padding spaces
|
|
178
|
+
const avail = w - 4;
|
|
179
|
+
const lv = [...lRaw].length;
|
|
180
|
+
const cv = [...cRaw].length;
|
|
181
|
+
const rv = [...rRaw].length;
|
|
182
|
+
|
|
183
|
+
const remaining = avail - lv - cv - rv;
|
|
184
|
+
const g1 = remaining > 0 ? Math.floor(remaining / 2) : 0;
|
|
185
|
+
const g2 = remaining > 0 ? remaining - g1 : 0;
|
|
186
|
+
|
|
187
|
+
return chalk.hex('#303134')('│') + ' ' +
|
|
188
|
+
lC + ' '.repeat(g1) + cC + ' '.repeat(g2) + rC + ' ' +
|
|
189
|
+
chalk.hex('#303134')('│');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Bottom command palette ────────────────────────────────────────────────────
|
|
193
|
+
/**
|
|
194
|
+
* Returns a single-line bottom palette string (no trailing newline).
|
|
195
|
+
* Caller is responsible for writing '\n'.
|
|
196
|
+
*/
|
|
197
|
+
export function bottomPalette() {
|
|
198
|
+
const accent = chalk.hex('#8AB4F8').bold;
|
|
199
|
+
const desc = chalk.hex('#80868B');
|
|
200
|
+
const sep = chalk.hex('#303134')(' · ');
|
|
201
|
+
const items = [
|
|
202
|
+
accent('F1') + desc(' Docs'),
|
|
203
|
+
accent('F2') + desc(' Logs'),
|
|
204
|
+
accent('F3') + desc(' Rollback'),
|
|
205
|
+
accent('F4') + desc(' Bulk Sweep'),
|
|
206
|
+
accent('Ctrl+C') + desc(' Exit'),
|
|
207
|
+
];
|
|
208
|
+
return ' ' + items.join(sep);
|
|
209
|
+
}
|