pelulu-cli 1.0.0 → 1.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.
Files changed (43) hide show
  1. package/README.md +69 -82
  2. package/config.example.json +2 -1
  3. package/package.json +6 -3
  4. package/preview.jpg +0 -0
  5. package/src/core/auto-format.js +61 -12
  6. package/src/core/config.js +1 -1
  7. package/src/core/confirm.js +2 -2
  8. package/src/core/diff.js +4 -4
  9. package/src/core/doctor.js +2 -2
  10. package/src/core/error-handler.js +1 -1
  11. package/src/core/file-tracker.js +4 -4
  12. package/src/core/formatter.js +19 -19
  13. package/src/core/logger.js +19 -3
  14. package/src/core/spinner.js +2 -2
  15. package/src/core/stats.js +2 -2
  16. package/src/core/thinking.js +8 -8
  17. package/src/core/tool-help.js +2 -2
  18. package/src/core/tool-registry.js +6 -10
  19. package/src/core/update-checker.js +125 -0
  20. package/src/core/wizard.js +5 -5
  21. package/src/core/workspace.js +4 -4
  22. package/src/index.js +89 -37
  23. package/src/mcp/activation.js +2 -5
  24. package/src/mcp/message-sender.js +1 -1
  25. package/src/mcp/mqtt-client.js +4 -4
  26. package/src/repl-commands.js +2 -1
  27. package/src/repl.js +26 -6
  28. package/src/tools/config.js +1 -1
  29. package/src/tools/diff.js +6 -0
  30. package/src/tools/file.js +3 -3
  31. package/src/tools/git.js +1 -1
  32. package/src/tools/network.js +1 -1
  33. package/src/tools/project.js +2 -2
  34. package/src/tools/search.js +3 -3
  35. package/src/tools/shell.js +2 -2
  36. package/src/tools/template.js +1 -1
  37. package/src/tools/watch.js +1 -1
  38. package/src/tui/completable-input.js +127 -0
  39. package/src/tui/ink-app.js +324 -0
  40. package/src/tui/ink-components.js +157 -0
  41. package/src/tui/ink-entry.js +86 -0
  42. package/src/tui/renderer.js +137 -19
  43. package/src/tui/status-bar.js +7 -7
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Ink UI Components — building blocks for Pelulu TUI
3
+ * Uses React.createElement (no JSX — ESM compatible, no build step)
4
+ */
5
+ import React, { useState, useEffect, useRef } from 'react';
6
+ import { Box, Text, useStdin } from 'ink';
7
+ import TextInput from 'ink-text-input';
8
+
9
+ // ─── Status Bar ───────────────────────────────────────────
10
+ export function StatusBar({ connected, session, version }) {
11
+ return React.createElement(Box, {
12
+ borderStyle: 'single', borderColor: 'cyan', width: '100%', paddingX: 0,
13
+ },
14
+ React.createElement(Text, { color: 'cyan', bold: true }, ` pelulu-cli v.${version || '0.0.0'} `),
15
+ React.createElement(Text, { dimColor: true }, '| '),
16
+ React.createElement(Text, { color: connected ? 'green' : 'red', bold: connected },
17
+ connected ? 'ONLINE' : 'OFFLINE'
18
+ ),
19
+ React.createElement(Text, { dimColor: true }, ' | '),
20
+ React.createElement(Text, { dimColor: true }, session ? session.slice(0, 8) : '-'),
21
+ React.createElement(Text, { dimColor: true }, ' | Provider: Xiaozhi.me'),
22
+ );
23
+ }
24
+
25
+ // ─── Strip Emojis ─────────────────────────────────────────
26
+ function stripEmojis(text) {
27
+ return text
28
+ .replace(/\p{Emoji_Presentation}/gu, '')
29
+ .replace(/\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?/gu, '')
30
+ .replace(/\p{Extended_Pictographic}/gu, '')
31
+ .replace(/[\u2600-\u27BF]/g, '')
32
+ .replace(/[\u{1F000}-\u{1FFFF}]/gu, '')
33
+ .replace(/[\u200D\uFE0F]/g, '')
34
+ .replace(/[ \t]{2,}/g, ' ')
35
+ .replace(/ ([.,;:!?])/g, '$1')
36
+ .trim();
37
+ }
38
+
39
+ // ─── Wrap text helper ─────────────────────────────────────
40
+ function wrapText(text, maxWidth) {
41
+ const lines = [];
42
+ for (const paragraph of text.split('\n')) {
43
+ if (!paragraph.trim()) { lines.push(''); continue; }
44
+ const words = paragraph.split(/\s+/);
45
+ let line = '';
46
+ for (const word of words) {
47
+ if (line.length + word.length + 1 > maxWidth && line.length > 0) {
48
+ lines.push(line);
49
+ line = word;
50
+ } else {
51
+ line = line ? `${line} ${word}` : word;
52
+ }
53
+ }
54
+ if (line) lines.push(line);
55
+ }
56
+ return lines;
57
+ }
58
+
59
+ // ─── Message Bubble ───────────────────────────────────────
60
+ export function MessageBubble({ message }) {
61
+ const { role, content } = message;
62
+ const isUser = role === 'user';
63
+ const isTool = role === 'tool';
64
+ const isSystem = role === 'system';
65
+
66
+ if (isTool) {
67
+ return React.createElement(Box, { paddingLeft: 2 },
68
+ React.createElement(Text, { dimColor: true },
69
+ `${message.toolName || 'tool'}${message.action ? '.' + message.action : ''} ${message.detail || ''}`
70
+ ),
71
+ );
72
+ }
73
+
74
+ if (isSystem) {
75
+ return React.createElement(Box, { paddingLeft: 2 },
76
+ React.createElement(Text, { dimColor: true, italic: true }, content),
77
+ );
78
+ }
79
+
80
+ const cleanContent = stripEmojis(content);
81
+ if (!cleanContent) return null;
82
+
83
+ const color = isUser ? 'blue' : 'white';
84
+ const w = (process.stdout.columns || 80) - 6;
85
+
86
+ if (isUser) {
87
+ return React.createElement(Box, { paddingLeft: 1, flexDirection: 'column' },
88
+ React.createElement(Text, { color, bold: true }, `> ${cleanContent}`),
89
+ );
90
+ }
91
+
92
+ // Assistant: wrap + indent all lines
93
+ const lines = wrapText(cleanContent, w);
94
+ return React.createElement(Box, { paddingLeft: 2, flexDirection: 'column' },
95
+ ...lines.map((line, i) =>
96
+ React.createElement(Text, { key: i, color }, line)
97
+ ),
98
+ );
99
+ }
100
+
101
+ // ─── Tool Result Line ─────────────────────────────────────
102
+ export function ToolResultLine({ success, detail }) {
103
+ return React.createElement(Box, { paddingLeft: 4 },
104
+ React.createElement(Text, { color: success ? 'green' : 'red' },
105
+ success ? `v ${detail || 'OK'}` : `x ${detail || 'error'}`
106
+ ),
107
+ );
108
+ }
109
+
110
+ // ─── Input Bar ────────────────────────────────────────────
111
+ export function InputBar({ onSubmit, placeholder }) {
112
+ const [value, setValue] = useState('');
113
+
114
+ const handleSubmit = (val) => {
115
+ const trimmed = val.trim();
116
+ if (!trimmed) return;
117
+ onSubmit(trimmed);
118
+ setValue('');
119
+ };
120
+
121
+ return React.createElement(Box, { paddingX: 1 },
122
+ React.createElement(Text, { color: 'cyan' }, '> '),
123
+ React.createElement(TextInput, {
124
+ value,
125
+ onChange: setValue,
126
+ onSubmit: handleSubmit,
127
+ placeholder: placeholder || 'type a message...',
128
+ }),
129
+ );
130
+ }
131
+
132
+ // ─── Thinking Indicator ───────────────────────────────────
133
+ export function ThinkingIndicator({ state }) {
134
+ if (!state || state === 'idle') return null;
135
+
136
+ const labels = {
137
+ thinking: 'thinking...',
138
+ tool_call: 'using tool...',
139
+ reading: 'reading file...',
140
+ writing: 'writing...',
141
+ searching: 'searching...',
142
+ };
143
+
144
+ return React.createElement(Box, { paddingLeft: 2 },
145
+ React.createElement(Text, { dimColor: true, italic: true },
146
+ `.. ${labels[state] || 'processing...'}`
147
+ ),
148
+ );
149
+ }
150
+
151
+ // ─── Divider ──────────────────────────────────────────────
152
+ export function Divider() {
153
+ return React.createElement(Box, { width: '100%' },
154
+ React.createElement(Text, { dimColor: true }, '─'.repeat(process.stdout.columns || 80)),
155
+ );
156
+ }
157
+
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Ink Entry — renders the Ink TUI and connects to Pelulu internals
3
+ * Falls back to readline REPL if Ink can't start (non-TTY, etc.)
4
+ */
5
+ import React from 'react';
6
+ import { render } from 'ink';
7
+ import { createApp } from './ink-app.js';
8
+
9
+ export function startInkTUI({ registry, mqtt, stats, session, bus, config, extras }) {
10
+ const isTTY = process.stdin.isTTY;
11
+
12
+ if (!isTTY) {
13
+ return startFallbackREPL({ registry, mqtt, stats, session, bus, config, extras });
14
+ }
15
+
16
+ const App = createApp({ registry, mqtt, stats, session, bus, config, extras });
17
+
18
+ const { unmount, waitUntilExit } = render(
19
+ React.createElement(App),
20
+ { exitOnCtrlC: false },
21
+ );
22
+ return { unmount, waitUntilExit };
23
+ }
24
+
25
+ async function startFallbackREPL({ registry, mqtt, stats, session, bus, extras }) {
26
+ const { createInterface } = await import('readline');
27
+ const chalk = (await import('chalk')).default;
28
+ const { getCompletions } = await import('../core/completion.js');
29
+
30
+ console.log(chalk.gray('\n Non-interactive mode. Type to chat.\n'));
31
+
32
+ const rl = createInterface({
33
+ input: process.stdin, output: process.stdout,
34
+ prompt: chalk.cyan('> '),
35
+ completer: (line) => {
36
+ const hits = getCompletions(line);
37
+ return [hits.length ? hits : [], line];
38
+ },
39
+ });
40
+
41
+ bus.on('llm:text', (text) => {
42
+ const clean = text.replace(/\p{Emoji_Presentation}/gu, '').replace(/\p{Extended_Pictographic}/gu, '').trim();
43
+ if (clean) console.log(chalk.white(` ${clean}`));
44
+ rl.prompt();
45
+ });
46
+
47
+ bus.on('tool:called', ({ name, result, args }) => {
48
+ const status = result.isError ? chalk.red('x') : chalk.green('v');
49
+ const detail = args?.path || args?.command || '';
50
+ console.log(chalk.dim(` ${name}.${args?.action || ''} ${detail} ${status}`));
51
+ });
52
+
53
+ bus.on('ready', () => rl.prompt());
54
+
55
+ rl.on('line', async (input) => {
56
+ const text = input.trim();
57
+ if (!text) return rl.prompt();
58
+
59
+ if (text.startsWith('/quit') || text.startsWith('/exit')) {
60
+ process.exit(0);
61
+ }
62
+
63
+ // Intent parsing
64
+ try {
65
+ const { parseIntent } = await import('../core/intent.js');
66
+ const intent = parseIntent(text);
67
+ if (intent.matched) {
68
+ const result = await registry.call(intent.tool, intent.params);
69
+ const { formatToolResult } = await import('../core/formatter.js');
70
+ console.log(`\n${formatToolResult(intent.tool, intent.action, result)}\n`);
71
+ return rl.prompt();
72
+ }
73
+ } catch {}
74
+
75
+ mqtt.sendText(text);
76
+ rl.prompt();
77
+ });
78
+
79
+ rl.on('close', () => process.exit(0));
80
+ rl.prompt();
81
+
82
+ return {
83
+ unmount: () => rl.close(),
84
+ waitUntilExit: () => new Promise(() => {}),
85
+ };
86
+ }
@@ -23,19 +23,38 @@ function center(text, width) {
23
23
  return ' '.repeat(left) + text + ' '.repeat(width - left - text.length);
24
24
  }
25
25
 
26
- export function renderBanner(config, tools, connected) {
26
+ /**
27
+ * Render the Pelulu ASCII art banner (displayed once at startup)
28
+ */
29
+ export function renderAsciiBanner(version) {
30
+ const v = version ? `v.${version}` : '';
31
+ const lines = [
32
+ chalk.cyan(' /\\_/\\ '),
33
+ chalk.cyan(' ( o.o ) ') + chalk.bold.cyan(` P E L U L U - C L I`),
34
+ chalk.cyan(' > ^ < ') + chalk.gray(` ${v} | powered by XiaoZhi`),
35
+ chalk.cyan(' /| |\\ '),
36
+ chalk.cyan('(_| |_)') + chalk.dim(' the tiny coding companion'),
37
+ ];
38
+ console.log('');
39
+ for (const line of lines) console.log(line);
40
+ console.log('');
41
+ }
42
+
43
+ export function renderBanner(config, tools, connected, meta = {}) {
27
44
  const w = 48;
28
45
  const actions = tools.reduce((s, t) => s + (t.actions?.length || 0), 0);
29
46
  const cwd = process.cwd();
30
47
  const dirName = cwd.split('/').pop() || cwd;
48
+ const session = meta.session || '-';
49
+ const version = config.agent?.version || '';
31
50
 
32
51
  console.log('');
33
52
  console.log(chalk.cyan(`${box.tl}${horizontal(w, box.h)}${box.tr}`));
34
- console.log(chalk.cyan(`${box.v}`) + chalk.bold.white(center(`🐾 ${config.agent?.name || 'Pelulu CLI'}`, w)) + chalk.cyan(`${box.v}`));
53
+ console.log(chalk.cyan(`${box.v}`) + chalk.bold.white(center(`${config.agent?.name || 'Pelulu CLI'}${version ? ' v' + version : ''}`, w)) + chalk.cyan(`${box.v}`));
35
54
  console.log(chalk.cyan(`${box.ml}${horizontal(w, box.h)}${box.mr}`));
36
- console.log(chalk.cyan(`${box.v}`) + chalk.gray(pad(` 📁 ${dirName}`, w)) + chalk.cyan(`${box.v}`));
37
- console.log(chalk.cyan(`${box.v}`) + chalk.gray(pad(` 🔧 ${tools.length} tools · ${actions} actions · 15 MCP slots used`, w)) + chalk.cyan(`${box.v}`));
38
- console.log(chalk.cyan(`${box.v}`) + chalk.gray(pad(` ${connected ? '🟢 MQTT Connected' : '🔴 Disconnected'}`, w)) + chalk.cyan(`${box.v}`));
55
+ console.log(chalk.cyan(`${box.v}`) + chalk.gray(pad(` ${dirName}`, w)) + chalk.cyan(`${box.v}`));
56
+ console.log(chalk.cyan(`${box.v}`) + chalk.gray(pad(` ${tools.length} tools / ${actions} actions`, w)) + chalk.cyan(`${box.v}`));
57
+ console.log(chalk.cyan(`${box.v}`) + chalk.gray(pad(` ${connected ? 'MQTT: on' : 'MQTT: off'} | Session: ${session}`, w)) + chalk.cyan(`${box.v}`));
39
58
  console.log(chalk.cyan(`${box.bl}${horizontal(w, box.h)}${box.br}`));
40
59
  console.log('');
41
60
  }
@@ -44,7 +63,7 @@ export function renderStatus(status) {
44
63
  const w = 48;
45
64
  console.log('');
46
65
  console.log(chalk.cyan(`${box.tl}${horizontal(w, box.h)}${box.tr}`));
47
- console.log(chalk.cyan(`${box.v}`) + chalk.bold.white(pad(' 📊 Status', w)) + chalk.cyan(`${box.v}`));
66
+ console.log(chalk.cyan(`${box.v}`) + chalk.bold.white(pad(' [STATS] Status', w)) + chalk.cyan(`${box.v}`));
48
67
  console.log(chalk.cyan(`${box.ml}${horizontal(w, box.h)}${box.mr}`));
49
68
  for (const [key, value] of Object.entries(status)) {
50
69
  console.log(chalk.cyan(`${box.v}`) + chalk.gray(pad(` ${key}: ${value}`, w)) + chalk.cyan(`${box.v}`));
@@ -55,7 +74,7 @@ export function renderStatus(status) {
55
74
 
56
75
  export function renderTools(tools) {
57
76
  console.log('');
58
- console.log(chalk.bold.white('🔧 Available Tools:'));
77
+ console.log(chalk.bold.white('Available Tools:'));
59
78
  console.log('');
60
79
  for (const t of tools) {
61
80
  const actions = t.actions?.map(a => a.name || a).join(', ') || '';
@@ -67,29 +86,73 @@ export function renderTools(tools) {
67
86
 
68
87
  export function renderToolCall(name, action, args) {
69
88
  const ts = new Date().toLocaleTimeString();
70
- console.log('');
71
- console.log(chalk.dim(` ${ts} `) + chalk.cyan('⚙️ ') + chalk.white(`${name}.${action}`));
72
- if (args?.path) console.log(chalk.dim(` 📁 ${args.path}`));
73
- if (args?.command) console.log(chalk.dim(` 💻 ${args.command}`));
89
+ const actionStr = action ? `.${action}` : '';
90
+ const detail = args?.path || args?.command || args?.pattern || args?.url || '';
91
+ const detailStr = detail ? chalk.dim(` ${detail}`) : '';
92
+ console.log(` ${chalk.dim(ts)} ${chalk.cyan(name)}${chalk.white(actionStr)}${detailStr}`);
74
93
  }
75
94
 
76
95
  export function renderToolResult(success, data) {
77
96
  if (success) {
78
- console.log(chalk.green(` OK`));
97
+ console.log(chalk.green(` [OK] OK`));
79
98
  } else {
80
- console.log(chalk.red(` ${data || 'error'}`));
99
+ console.log(chalk.red(` [ERR] ${data || 'error'}`));
81
100
  }
82
101
  }
83
102
 
103
+ /**
104
+ * Strip emoji and decorative unicode from text.
105
+ * Keeps ASCII, CJK, common punctuation.
106
+ */
107
+ function stripEmojis(text) {
108
+ // Remove emoji (Unicode ranges), box-drawing decorative symbols, and common decorative chars
109
+ return text
110
+ .replace(/\p{Emoji_Presentation}/gu, '')
111
+ .replace(/\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?/gu, '')
112
+ .replace(/\p{Extended_Pictographic}/gu, '')
113
+ .replace(/[\u2600-\u27BF]/g, '')
114
+ .replace(/[\u{1F000}-\u{1FFFF}]/gu, '')
115
+ .replace(/[\u200D\uFE0F]/g, '')
116
+ .replace(/[ \t]{2,}/g, ' ')
117
+ .replace(/ ([.,;:!?])/g, '$1')
118
+ .trim();
119
+ }
120
+
84
121
  export function renderAiResponse(text) {
122
+ const clean = stripEmojis(text);
123
+ if (!clean) return;
85
124
  console.log('');
86
- console.log(chalk.green(` 🤖 ${text}`));
125
+ // Wrap long lines at 80 chars
126
+ const lines = wrapText(clean, 80);
127
+ for (const line of lines) {
128
+ console.log(chalk.white(` ${line}`));
129
+ }
87
130
  console.log('');
88
131
  }
89
132
 
133
+ function wrapText(text, maxWidth) {
134
+ const paragraphs = text.split('\n');
135
+ const result = [];
136
+ for (const para of paragraphs) {
137
+ if (!para.trim()) { result.push(''); continue; }
138
+ const words = para.split(/\s+/);
139
+ let line = '';
140
+ for (const word of words) {
141
+ if (line.length + word.length + 1 > maxWidth && line.length > 0) {
142
+ result.push(line);
143
+ line = word;
144
+ } else {
145
+ line = line ? `${line} ${word}` : word;
146
+ }
147
+ }
148
+ if (line) result.push(line);
149
+ }
150
+ return result;
151
+ }
152
+
90
153
  export function renderUserInput(text) {
91
154
  const ts = new Date().toLocaleTimeString();
92
- console.log(chalk.dim(` ${ts} `) + chalk.blue('👤 ') + chalk.white(text));
155
+ console.log(chalk.dim(` ${ts} `) + chalk.blue('> ') + chalk.white(text));
93
156
  }
94
157
 
95
158
  export function renderHelp() {
@@ -103,17 +166,72 @@ export function renderHelp() {
103
166
  console.log(chalk.cyan(' /files') + chalk.gray(' File changes'));
104
167
  console.log(chalk.cyan(' /call <tool>') + chalk.gray(' Call tool directly'));
105
168
  console.log(chalk.cyan(' /doctor') + chalk.gray(' Health check'));
169
+ console.log(chalk.cyan(' /keys') + chalk.gray(' Keyboard shortcuts'));
106
170
  console.log(chalk.cyan(' /clear') + chalk.gray(' Clear screen'));
107
171
  console.log(chalk.cyan(' /quit') + chalk.gray(' Exit'));
108
172
  console.log('');
109
173
  console.log(chalk.bold.white(' Shortcuts:'));
110
- console.log(chalk.cyan(' read index.js') + chalk.gray('file read'));
111
- console.log(chalk.cyan(' run npm test') + chalk.gray('shell exec'));
112
- console.log(chalk.cyan(' git status') + chalk.gray(' git status'));
113
- console.log(chalk.cyan(' build') + chalk.gray(' project build'));
174
+ console.log(chalk.cyan(' read <file>') + chalk.gray(' file read'));
175
+ console.log(chalk.cyan(' run <cmd>') + chalk.gray(' shell exec'));
176
+ console.log(chalk.cyan(' git <cmd>') + chalk.gray(' git operation'));
177
+ console.log(chalk.cyan(' build / test / lint') + chalk.gray(' project actions'));
178
+ console.log(chalk.cyan(' Tab') + chalk.gray(' auto-complete'));
114
179
  console.log('');
115
180
  }
116
181
 
117
182
  export function createPrompt(dirName) {
118
183
  return chalk.cyan(`${dirName} `) + chalk.white('❯ ');
119
184
  }
185
+
186
+ /**
187
+ * Render update notification banner
188
+ * @param {object} update - { local, remote, release: { tag, name, url, body } }
189
+ */
190
+ export function renderUpdateNotification(update) {
191
+ const w = 56;
192
+ const { local, remote, release } = update;
193
+
194
+ console.log('');
195
+ console.log(chalk.yellow(`${box.tl}${horizontal(w, box.h)}${box.tr}`));
196
+ console.log(chalk.yellow(`${box.v}`) + chalk.bold.yellow(center(' UPDATE TERSEDIA!', w)) + chalk.yellow(`${box.v}`));
197
+ console.log(chalk.yellow(`${box.ml}${horizontal(w, box.h)}${box.mr}`));
198
+ console.log(chalk.yellow(`${box.v}`) + chalk.white(pad(` Versi lokal : ${local}`, w)) + chalk.yellow(`${box.v}`));
199
+ console.log(chalk.yellow(`${box.v}`) + chalk.green(pad(` Versi terbaru : ${remote}`, w)) + chalk.yellow(`${box.v}`));
200
+
201
+ if (release?.name && release.name !== release.tag) {
202
+ console.log(chalk.yellow(`${box.v}`) + chalk.gray(pad(` Release : ${release.name}`, w)) + chalk.yellow(`${box.v}`));
203
+ }
204
+
205
+ if (release?.url) {
206
+ console.log(chalk.yellow(`${box.v}`) + chalk.cyan(pad(` ${release.url}`, w)) + chalk.yellow(`${box.v}`));
207
+ }
208
+
209
+ console.log(chalk.yellow(`${box.ml}${horizontal(w, box.h)}${box.mr}`));
210
+ console.log(chalk.yellow(`${box.v}`) + chalk.bold.white(pad(' Jalankan perintah berikut untuk update:', w)) + chalk.yellow(`${box.v}`));
211
+ console.log(chalk.yellow(`${box.v}`) + chalk.green(pad(' npm install pelulu-cli@latest', w)) + chalk.yellow(`${box.v}`));
212
+ console.log(chalk.yellow(`${box.bl}${horizontal(w, box.h)}${box.br}`));
213
+ console.log('');
214
+ }
215
+
216
+ /**
217
+ * Render update check failure (silent, dim)
218
+ */
219
+ export function renderUpdateError(message) {
220
+ console.log(chalk.dim(` [WARN] Update check failed: ${message}`));
221
+ }
222
+
223
+ /**
224
+ * Render a clean init status line (replaces verbose per-tool logging)
225
+ */
226
+ export function renderInitLine(icon, text, detail = '') {
227
+ const detailStr = detail ? chalk.dim(` (${detail})`) : '';
228
+ console.log(chalk.gray(` ${icon} ${text}`) + detailStr);
229
+ }
230
+
231
+ /**
232
+ * Render ready line with session info
233
+ */
234
+ export function renderReady(sessionId) {
235
+ console.log(chalk.green(` ✓ Ready`) + chalk.dim(` session: ${sessionId || '-'}`));
236
+ console.log('');
237
+ }
@@ -7,7 +7,7 @@ import chalk from 'chalk';
7
7
  export class StatusBar {
8
8
  constructor() {
9
9
  this.items = {
10
- mqtt: '',
10
+ mqtt: '...',
11
11
  session: '-',
12
12
  tools: 0,
13
13
  calls: 0,
@@ -22,12 +22,12 @@ export class StatusBar {
22
22
  const { mqtt, session, tools, calls } = this.items;
23
23
  const time = new Date().toLocaleTimeString();
24
24
  const line = [
25
- chalk.dim(''.repeat(process.stdout.columns || 60)),
26
- chalk.gray(` ${mqtt === '' ? '🟢' : '🔴'} MQTT: ${mqtt}`),
27
- chalk.gray(` 📡 Session: ${session}`),
28
- chalk.gray(` 🔧 Tools: ${tools}`),
29
- chalk.gray(` 📊 Calls: ${calls}`),
30
- chalk.gray(` 🕐 ${time}`),
25
+ chalk.dim('\u2500'.repeat(process.stdout.columns || 60)),
26
+ chalk.gray(` ${mqtt === '[OK]' ? '[on]' : '[off]'} MQTT: ${mqtt}`),
27
+ chalk.gray(` Session: ${session}`),
28
+ chalk.gray(` Tools: ${tools}`),
29
+ chalk.gray(` Calls: ${calls}`),
30
+ chalk.gray(` ${time}`),
31
31
  ].join(' ');
32
32
  console.log(line);
33
33
  }