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.
- package/CHANGELOG.md +81 -0
- package/package.json +10 -6
- package/src/agent/agent-controller.js +105 -0
- package/src/agent/agent-loop.js +248 -0
- package/src/agent/context-builder.js +307 -0
- package/src/agent/history-condenser.js +202 -0
- package/src/agent/index.js +8 -0
- package/src/agent/llm-client.js +44 -0
- package/src/agent/plan-manager.js +342 -0
- package/src/agent/system-prompt.js +41 -0
- package/src/core/config.js +1 -1
- package/src/core/http-client.js +285 -0
- package/src/core/job-manager.js +192 -0
- package/src/core/logger.js +106 -1
- package/src/core/system-prompt.js +24 -8
- package/src/core/tool-registry.js +65 -6
- package/src/index.js +151 -29
- package/src/mcp/mcp-handler.js +63 -4
- package/src/mcp/mqtt-client.js +195 -12
- package/src/tools/agent.js +80 -0
- package/src/tools/file.js +8 -2
- package/src/tools/git.js +56 -6
- package/src/tools/jobs.js +93 -0
- package/src/tools/network.js +56 -23
- package/src/tools/project.js +38 -4
- package/src/tools/search.js +10 -14
- package/src/tools/shell.js +70 -4
- package/src/tui/completable-input.js +49 -42
- package/src/tui/ink-app.js +358 -19
- package/src/tui/ink-components.js +82 -12
- package/src/tui/ink-entry.js +26 -2
- package/src/tui/renderer.js +80 -39
|
@@ -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
|
-
|
|
49
|
+
flexDirection: 'column', width: '100%',
|
|
50
|
+
borderStyle: 'single', borderColor: 'cyan',
|
|
51
|
+
paddingY: 0, paddingX: 0,
|
|
13
52
|
},
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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, '')
|
package/src/tui/ink-entry.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/tui/renderer.js
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
* Like Claude Code / Gemini CLI / OpenCode
|
|
4
4
|
*/
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
-
import {
|
|
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(
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
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('');
|