pelulu-cli 1.0.5 → 1.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.
@@ -5,25 +5,95 @@
5
5
  import React, { useState, useEffect, useRef } from 'react';
6
6
  import { Box, Text, useStdin } from 'ink';
7
7
  import TextInput from 'ink-text-input';
8
+ import { readFile } from 'fs/promises';
9
+ import { join, dirname } from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const ROOT = join(__dirname, '..', '..');
14
+
15
+ let _pkgVersion = null;
16
+ async function readPkgVersion() {
17
+ if (_pkgVersion) return _pkgVersion;
18
+ try {
19
+ const pkg = JSON.parse(await readFile(join(ROOT, 'package.json'), 'utf-8'));
20
+ _pkgVersion = pkg.version || '0.0.0';
21
+ } catch { _pkgVersion = '0.0.0'; }
22
+ return _pkgVersion;
23
+ }
24
+ readPkgVersion();
25
+
26
+ // ─── ASCII Banner ────────────────────────────────────────
27
+ // Memoized — only re-renders when version prop changes.
28
+ // Uses useMemo to cache the element reference so Ink's reconciler
29
+ // skips re-rendering on every parent state change (fixes banner duplication).
30
+ export const AsciiBanner = React.memo(function AsciiBanner({ version }) {
31
+ const v = version || _pkgVersion || '0.0.0';
32
+
33
+ const cat = [
34
+ ' /\\_/\\ ',
35
+ ' ( o.o ) ',
36
+ ' > ^ < ',
37
+ ' /| |\\ ',
38
+ ' (_| |_)',
39
+ ];
40
+ const info = [
41
+ 'P E L U L U - C L I',
42
+ `v${v}`,
43
+ 'the tiny coding companion',
44
+ 'powered by XiaoZhi',
45
+ '',
46
+ ];
8
47
 
9
- // ─── Status Bar ───────────────────────────────────────────
10
- export function StatusBar({ connected, session, version }) {
11
48
  return React.createElement(Box, {
12
- borderStyle: 'single', borderColor: 'cyan', width: '100%', paddingX: 0,
49
+ flexDirection: 'column', width: '100%',
50
+ borderStyle: 'single', borderColor: 'cyan',
51
+ paddingY: 0, paddingX: 0,
13
52
  },
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'
53
+ ...cat.map((line, i) =>
54
+ React.createElement(Box, { key: i, paddingLeft: 1 },
55
+ React.createElement(Text, { color: 'cyan' }, line),
56
+ React.createElement(Text, {
57
+ color: i === 0 ? 'cyan' : i === 1 ? 'gray' : i === 2 ? 'cyanBright' : 'gray',
58
+ bold: i === 0,
59
+ }, ' ' + (info[i] || '')),
60
+ )
61
+ ),
62
+ React.createElement(Box, { paddingLeft: 1 },
63
+ React.createElement(Text, { dimColor: true }, ' 18 tools • MCP protocol • agent mode • xiaozhi.me'),
18
64
  ),
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
65
  );
23
- }
66
+ });
67
+
68
+ // ─── Status Bar ───────────────────────────────────────────
69
+ // Uses useMemo to cache the element — avoids re-rendering on parent state changes
70
+ export const StatusBar = React.memo(function StatusBar({ connected, session }) {
71
+ const statusDot = connected ? '●' : '○';
72
+ const statusColor = connected ? 'green' : 'red';
73
+ const sess = session ? session.slice(0, 8) : '---';
74
+ const label = connected ? ' online' : ' offline';
75
+
76
+ const element = React.useMemo(() => React.createElement(Box, {
77
+ borderStyle: 'single', borderColor: 'cyan', width: '100%',
78
+ paddingX: 1, paddingY: 0,
79
+ flexDirection: 'row', justifyContent: 'space-between',
80
+ },
81
+ React.createElement(Box, { flexDirection: 'row' },
82
+ React.createElement(Text, { color: 'cyan', bold: true }, 'PELULU '),
83
+ React.createElement(Text, { color: statusColor }, statusDot),
84
+ React.createElement(Text, { color: statusColor, bold: connected }, label),
85
+ ),
86
+ React.createElement(Box, { flexDirection: 'row' },
87
+ React.createElement(Text, { dimColor: true }, `session:${sess}`),
88
+ React.createElement(Text, { dimColor: true }, ' '),
89
+ React.createElement(Text, { dimColor: true }, 'xiaozhi.me'),
90
+ ),
91
+ ), [connected, sess]);
92
+ return element;
93
+ });
24
94
 
25
95
  // ─── Strip Emojis ─────────────────────────────────────────
26
- function stripEmojis(text) {
96
+ export function stripEmojis(text) {
27
97
  return text
28
98
  .replace(/\p{Emoji_Presentation}/gu, '')
29
99
  .replace(/\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?/gu, '')
@@ -10,7 +10,15 @@ export function startInkTUI({ registry, mqtt, stats, session, bus, config, extra
10
10
  const isTTY = process.stdin.isTTY;
11
11
 
12
12
  if (!isTTY) {
13
- return startFallbackREPL({ registry, mqtt, stats, session, bus, config, extras });
13
+ // Return a wrapper that handles the async REPL
14
+ let replResult = null;
15
+ const replPromise = startFallbackREPL({ registry, mqtt, stats, session, bus, config, extras })
16
+ .then(result => { replResult = result; return result; });
17
+
18
+ return {
19
+ unmount: () => { if (replResult) replResult.unmount(); },
20
+ waitUntilExit: () => replPromise,
21
+ };
14
22
  }
15
23
 
16
24
  const App = createApp({ registry, mqtt, stats, session, bus, config, extras });
@@ -72,7 +80,23 @@ async function startFallbackREPL({ registry, mqtt, stats, session, bus, extras }
72
80
  }
73
81
  } catch {}
74
82
 
75
- mqtt.sendText(text);
83
+ // Use agent controller if available, otherwise send directly
84
+ const agentController = extras?.agentController;
85
+ if (agentController) {
86
+ try {
87
+ const result = await agentController.run(text, { generatePlan: false });
88
+ if (result.success && result.result) {
89
+ const clean = result.result.replace(/\p{Emoji_Presentation}/gu, '').replace(/\p{Extended_Pictographic}/gu, '').trim();
90
+ if (clean) console.log(chalk.white(` ${clean}`));
91
+ } else if (!result.success) {
92
+ console.log(chalk.red(` Error: ${result.result}`));
93
+ }
94
+ } catch (err) {
95
+ console.log(chalk.red(` Error: ${err.message}`));
96
+ }
97
+ } else {
98
+ mqtt.sendText(text);
99
+ }
76
100
  rl.prompt();
77
101
  });
78
102
 
@@ -3,7 +3,12 @@
3
3
  * Like Claude Code / Gemini CLI / OpenCode
4
4
  */
5
5
  import chalk from 'chalk';
6
- import { createInterface } from 'readline';
6
+ import { readFile } from 'fs/promises';
7
+ import { join, dirname } from 'path';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const ROOT = join(__dirname, '..', '..');
7
12
 
8
13
  const box = {
9
14
  tl: '╭', tr: '╮', bl: '╰', br: '╯',
@@ -23,20 +28,64 @@ function center(text, width) {
23
28
  return ' '.repeat(left) + text + ' '.repeat(width - left - text.length);
24
29
  }
25
30
 
31
+ /**
32
+ * Read version from package.json (cached)
33
+ */
34
+ let _cachedVersion = null;
35
+ async function getVersion() {
36
+ if (_cachedVersion) return _cachedVersion;
37
+ try {
38
+ const pkg = JSON.parse(await readFile(join(ROOT, 'package.json'), 'utf-8'));
39
+ _cachedVersion = pkg.version || '0.0.0';
40
+ } catch {
41
+ _cachedVersion = '0.0.0';
42
+ }
43
+ return _cachedVersion;
44
+ }
45
+
46
+ function stripAnsi(str) {
47
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
48
+ }
49
+
26
50
  /**
27
51
  * Render the Pelulu ASCII art banner (displayed once at startup)
28
52
  */
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'),
53
+ export async function renderAsciiBanner() {
54
+ const version = await getVersion();
55
+ const w = 52;
56
+
57
+ const cat = [
58
+ ' /\\_/\\ ',
59
+ ' ( o.o ) ',
60
+ ' > ^ < ',
61
+ ' /| |\\ ',
62
+ ' (_| |_)',
37
63
  ];
64
+
65
+ const info = [
66
+ chalk.cyan.bold('P E L U L U - C L I'),
67
+ chalk.gray(`v${version}`),
68
+ chalk.cyan('the tiny coding companion'),
69
+ chalk.gray('powered by XiaoZhi'),
70
+ '',
71
+ ];
72
+
38
73
  console.log('');
39
- for (const line of lines) console.log(line);
74
+ console.log(chalk.cyan(`${box.tl}${horizontal(w, box.h)}${box.tr}`));
75
+
76
+ for (let i = 0; i < Math.max(cat.length, info.length); i++) {
77
+ const left = cat[i] ? chalk.cyan(cat[i]) : ' '.repeat(11);
78
+ const right = info[i] || '';
79
+ const gap = ' ';
80
+ console.log(chalk.cyan(`${box.v}`) + left + gap + right + ' '.repeat(Math.max(0, w - 11 - gap.length - stripAnsi(right).length)) + chalk.cyan(`${box.v}`));
81
+ }
82
+
83
+ console.log(chalk.cyan(`${box.ml}${horizontal(w, box.h)}${box.mr}`));
84
+
85
+ const features = chalk.gray(' 18 tools • MCP protocol • agent mode');
86
+ console.log(chalk.cyan(`${box.v}`) + features + ' '.repeat(Math.max(0, w - stripAnsi(features).length)) + chalk.cyan(`${box.v}`));
87
+
88
+ console.log(chalk.cyan(`${box.bl}${horizontal(w, box.h)}${box.br}`));
40
89
  console.log('');
41
90
  }
42
91
 
@@ -100,12 +149,7 @@ export function renderToolResult(success, data) {
100
149
  }
101
150
  }
102
151
 
103
- /**
104
- * Strip emoji and decorative unicode from text.
105
- * Keeps ASCII, CJK, common punctuation.
106
- */
107
152
  function stripEmojis(text) {
108
- // Remove emoji (Unicode ranges), box-drawing decorative symbols, and common decorative chars
109
153
  return text
110
154
  .replace(/\p{Emoji_Presentation}/gu, '')
111
155
  .replace(/\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?/gu, '')
@@ -122,7 +166,6 @@ export function renderAiResponse(text) {
122
166
  const clean = stripEmojis(text);
123
167
  if (!clean) return;
124
168
  console.log('');
125
- // Wrap long lines at 80 chars
126
169
  const lines = wrapText(clean, 80);
127
170
  for (const line of lines) {
128
171
  console.log(chalk.white(` ${line}`));
@@ -183,10 +226,6 @@ export function createPrompt(dirName) {
183
226
  return chalk.cyan(`${dirName} `) + chalk.white('❯ ');
184
227
  }
185
228
 
186
- /**
187
- * Render update notification banner
188
- * @param {object} update - { local, remote, release: { tag, name, url, body } }
189
- */
190
229
  export function renderUpdateNotification(update) {
191
230
  const w = 56;
192
231
  const { local, remote, release } = update;
@@ -197,40 +236,42 @@ export function renderUpdateNotification(update) {
197
236
  console.log(chalk.yellow(`${box.ml}${horizontal(w, box.h)}${box.mr}`));
198
237
  console.log(chalk.yellow(`${box.v}`) + chalk.white(pad(` Versi lokal : ${local}`, w)) + chalk.yellow(`${box.v}`));
199
238
  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}`));
239
+ console.log(chalk.yellow(`${box.v}`) + chalk.gray(pad(' Menginstall update secara otomatis...', w)) + chalk.yellow(`${box.v}`));
212
240
  console.log(chalk.yellow(`${box.bl}${horizontal(w, box.h)}${box.br}`));
213
241
  console.log('');
214
242
  }
215
243
 
216
244
  /**
217
- * Render update check failure (silent, dim)
245
+ * Render usage info after successful update
218
246
  */
247
+ export function renderPostUpdate(packageName, version) {
248
+ const w = 52;
249
+ console.log('');
250
+ console.log(chalk.green(`${box.tl}${horizontal(w, box.h)}${box.tr}`));
251
+ console.log(chalk.green(`${box.v}`) + chalk.bold.green(center(` ✓ ${packageName} v${version} installed!`, w)) + chalk.green(`${box.v}`));
252
+ console.log(chalk.green(`${box.ml}${horizontal(w, box.h)}${box.mr}`));
253
+ console.log(chalk.green(`${box.v}`) + chalk.white(pad(' Run:', w)) + chalk.green(`${box.v}`));
254
+ console.log(chalk.green(`${box.v}`) + chalk.cyan(pad(` $ ${packageName}`, w)) + chalk.green(`${box.v}`));
255
+ console.log(chalk.green(`${box.v}`) + chalk.gray(pad('', w)) + chalk.green(`${box.v}`));
256
+ console.log(chalk.green(`${box.v}`) + chalk.white(pad(' Commands:', w)) + chalk.green(`${box.v}`));
257
+ console.log(chalk.green(`${box.v}`) + chalk.gray(pad(' /help — show commands', w)) + chalk.green(`${box.v}`));
258
+ console.log(chalk.green(`${box.v}`) + chalk.gray(pad(' /tools — list tools', w)) + chalk.green(`${box.v}`));
259
+ console.log(chalk.green(`${box.v}`) + chalk.gray(pad(' /status — connection status', w)) + chalk.green(`${box.v}`));
260
+ console.log(chalk.green(`${box.v}`) + chalk.gray(pad(' /clear — clear screen', w)) + chalk.green(`${box.v}`));
261
+ console.log(chalk.green(`${box.v}`) + chalk.gray(pad(' /quit — exit', w)) + chalk.green(`${box.v}`));
262
+ console.log(chalk.green(`${box.bl}${horizontal(w, box.h)}${box.br}`));
263
+ console.log('');
264
+ }
265
+
219
266
  export function renderUpdateError(message) {
220
267
  console.log(chalk.dim(` [WARN] Update check failed: ${message}`));
221
268
  }
222
269
 
223
- /**
224
- * Render a clean init status line (replaces verbose per-tool logging)
225
- */
226
270
  export function renderInitLine(icon, text, detail = '') {
227
271
  const detailStr = detail ? chalk.dim(` (${detail})`) : '';
228
272
  console.log(chalk.gray(` ${icon} ${text}`) + detailStr);
229
273
  }
230
274
 
231
- /**
232
- * Render ready line with session info
233
- */
234
275
  export function renderReady(sessionId) {
235
276
  console.log(chalk.green(` ✓ Ready`) + chalk.dim(` session: ${sessionId || '-'}`));
236
277
  console.log('');