pelulu-cli 1.0.3 → 1.1.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/docs/AGENT.md +237 -0
- package/package.json +3 -2
- package/specifications/xiaozhi-mqtt-broker.md +46 -0
- package/src/agent/agent-controller.js +100 -0
- package/src/agent/agent-loop.js +276 -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 +50 -0
- package/src/agent/plan-manager.js +342 -0
- package/src/agent/system-prompt.js +41 -0
- package/src/core/logger.js +106 -1
- package/src/core/system-prompt.js +24 -8
- package/src/core/tool-registry.js +13 -5
- package/src/core/update-checker.js +37 -31
- package/src/index.js +82 -22
- package/src/mcp/mcp-handler.js +29 -2
- package/src/mcp/mqtt-client.js +15 -3
- package/src/tools/agent.js +80 -0
- package/src/tui/completable-input.js +49 -42
- package/src/tui/ink-app.js +91 -12
- package/src/tui/ink-components.js +58 -11
- package/src/tui/ink-entry.js +26 -2
- package/src/tui/renderer.js +80 -39
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Tool — Expose agent capabilities (1 MCP tool, 5 actions)
|
|
3
|
+
* Actions: run, status, abort, reset, context
|
|
4
|
+
*/
|
|
5
|
+
import { bus } from '../core/event-bus.js';
|
|
6
|
+
|
|
7
|
+
let agentController = null;
|
|
8
|
+
|
|
9
|
+
export function setAgentController(controller) {
|
|
10
|
+
agentController = controller;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ACTIONS = {
|
|
14
|
+
run: {
|
|
15
|
+
required: ['task'],
|
|
16
|
+
handler: async ({ task }) => {
|
|
17
|
+
if (!agentController) throw new Error('Agent not initialized');
|
|
18
|
+
const result = await agentController.run(task, { generatePlan: false });
|
|
19
|
+
return { success: result.success, result: result.result, iterations: result.iterations };
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
status: {
|
|
24
|
+
required: [],
|
|
25
|
+
handler: async () => {
|
|
26
|
+
if (!agentController) throw new Error('Agent not initialized');
|
|
27
|
+
return agentController.summary;
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
abort: {
|
|
32
|
+
required: [],
|
|
33
|
+
handler: async () => {
|
|
34
|
+
if (!agentController) throw new Error('Agent not initialized');
|
|
35
|
+
agentController.abort();
|
|
36
|
+
return { aborted: true };
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
reset: {
|
|
41
|
+
required: [],
|
|
42
|
+
handler: async () => {
|
|
43
|
+
if (!agentController) throw new Error('Agent not initialized');
|
|
44
|
+
agentController.reset();
|
|
45
|
+
return { reset: true };
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
context: {
|
|
50
|
+
required: [],
|
|
51
|
+
handler: async () => {
|
|
52
|
+
if (!agentController) throw new Error('Agent not initialized');
|
|
53
|
+
return { context: await agentController.getContext() };
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const actionNames = Object.keys(ACTIONS);
|
|
59
|
+
|
|
60
|
+
export default {
|
|
61
|
+
name: 'agent',
|
|
62
|
+
description: 'Agent: run task, status, abort, reset, context',
|
|
63
|
+
actions: actionNames.map(name => ({ name, required: ACTIONS[name].required })),
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
action: { type: 'string' },
|
|
68
|
+
task: { type: 'string' },
|
|
69
|
+
},
|
|
70
|
+
required: ['action'],
|
|
71
|
+
},
|
|
72
|
+
async handler({ action, ...params }) {
|
|
73
|
+
const a = ACTIONS[action];
|
|
74
|
+
if (!a) throw new Error(`Unknown: ${action}. Use: ${actionNames.join(', ')}`);
|
|
75
|
+
for (const f of a.required) {
|
|
76
|
+
if (params[f] === undefined) throw new Error(`Missing: ${f}`);
|
|
77
|
+
}
|
|
78
|
+
return a.handler(params);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -1,68 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CompletableInput — TextInput with Tab auto-completion
|
|
2
|
+
* CompletableInput — TextInput with Tab auto-completion + char counter
|
|
3
3
|
* Wraps ink-text-input and adds completion support
|
|
4
4
|
*/
|
|
5
|
-
import React, { useState, useCallback } from 'react';
|
|
5
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
|
6
6
|
import { Box, Text, useInput } from 'ink';
|
|
7
7
|
import TextInput from 'ink-text-input';
|
|
8
8
|
import { getCompletions } from '../core/completion.js';
|
|
9
9
|
|
|
10
|
+
const MAX_CHARS = 70;
|
|
11
|
+
|
|
10
12
|
export function CompletableInput({ onSubmit, placeholder }) {
|
|
11
13
|
const [value, setValue] = useState('');
|
|
12
14
|
const [completions, setCompletions] = useState([]);
|
|
13
15
|
const [completionIndex, setCompletionIndex] = useState(0);
|
|
14
16
|
const [showCompletions, setShowCompletions] = useState(false);
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
const isAtLimit = value.length >= MAX_CHARS;
|
|
19
|
+
const charCount = value.length;
|
|
20
|
+
const remaining = MAX_CHARS - charCount;
|
|
21
|
+
|
|
22
|
+
// Handle Tab/Escape/Return — regular chars are handled by TextInput
|
|
17
23
|
useInput((input, key) => {
|
|
18
24
|
if (key.tab) {
|
|
19
25
|
if (!showCompletions) {
|
|
20
|
-
// First Tab: show completions
|
|
21
26
|
const hits = getCompletions(value);
|
|
22
27
|
if (hits.length > 0) {
|
|
23
28
|
setCompletions(hits);
|
|
24
29
|
setCompletionIndex(0);
|
|
25
30
|
setShowCompletions(true);
|
|
26
|
-
// Auto-fill first match
|
|
27
31
|
if (hits.length === 1) {
|
|
28
|
-
|
|
32
|
+
const filled = hits[0] + ' ';
|
|
33
|
+
if (filled.length <= MAX_CHARS) {
|
|
34
|
+
setValue(filled);
|
|
35
|
+
}
|
|
29
36
|
setShowCompletions(false);
|
|
30
37
|
} else {
|
|
31
|
-
setValue(hits[0]);
|
|
38
|
+
setValue(hits[0].slice(0, MAX_CHARS));
|
|
32
39
|
}
|
|
33
40
|
}
|
|
34
41
|
} else {
|
|
35
|
-
// Subsequent Tab: cycle through completions
|
|
36
42
|
const next = (completionIndex + 1) % completions.length;
|
|
37
43
|
setCompletionIndex(next);
|
|
38
|
-
setValue(completions[next]);
|
|
44
|
+
setValue(completions[next].slice(0, MAX_CHARS));
|
|
39
45
|
}
|
|
40
46
|
} else if (key.escape) {
|
|
41
|
-
// Escape: hide completions
|
|
42
47
|
setShowCompletions(false);
|
|
43
48
|
setCompletions([]);
|
|
44
49
|
setCompletionIndex(0);
|
|
45
50
|
} else if (key.return) {
|
|
46
|
-
// Enter: handled by TextInput onSubmit, do nothing here
|
|
47
51
|
setShowCompletions(false);
|
|
48
52
|
setCompletions([]);
|
|
49
|
-
} else {
|
|
50
|
-
// Any other key: reset completions
|
|
51
|
-
if (showCompletions) {
|
|
52
|
-
setShowCompletions(false);
|
|
53
|
-
setCompletions([]);
|
|
54
|
-
setCompletionIndex(0);
|
|
55
|
-
}
|
|
56
53
|
}
|
|
54
|
+
// All other keys: let TextInput handle them (no-op here)
|
|
57
55
|
});
|
|
58
56
|
|
|
59
|
-
// Update value from TextInput
|
|
57
|
+
// Update value from TextInput — stable reference, no deps
|
|
60
58
|
const handleChange = useCallback((val) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
const trimmed = val.slice(0, MAX_CHARS);
|
|
60
|
+
setValue(trimmed);
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
// Derive completions from value (debounced via useEffect to avoid
|
|
64
|
+
// extra re-renders on every character)
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (value.length > 0) {
|
|
67
|
+
const hits = getCompletions(value);
|
|
68
|
+
if (hits.length > 0 && hits[0] !== value) {
|
|
66
69
|
setCompletions(hits);
|
|
67
70
|
setShowCompletions(true);
|
|
68
71
|
setCompletionIndex(0);
|
|
@@ -72,9 +75,9 @@ export function CompletableInput({ onSubmit, placeholder }) {
|
|
|
72
75
|
} else {
|
|
73
76
|
setShowCompletions(false);
|
|
74
77
|
}
|
|
75
|
-
}, []);
|
|
78
|
+
}, [value]);
|
|
76
79
|
|
|
77
|
-
// Handle TextInput submit
|
|
80
|
+
// Handle TextInput submit
|
|
78
81
|
const handleSubmit = useCallback((val) => {
|
|
79
82
|
const trimmed = val.trim();
|
|
80
83
|
if (trimmed) {
|
|
@@ -85,6 +88,9 @@ export function CompletableInput({ onSubmit, placeholder }) {
|
|
|
85
88
|
setCompletions([]);
|
|
86
89
|
}, [onSubmit]);
|
|
87
90
|
|
|
91
|
+
// Color for char counter
|
|
92
|
+
const counterColor = isAtLimit ? 'red' : remaining <= 10 ? 'yellow' : 'dim';
|
|
93
|
+
|
|
88
94
|
return React.createElement(Box, { flexDirection: 'column', width: '100%' },
|
|
89
95
|
// Completion suggestions
|
|
90
96
|
showCompletions && completions.length > 1
|
|
@@ -105,23 +111,24 @@ export function CompletableInput({ onSubmit, placeholder }) {
|
|
|
105
111
|
)
|
|
106
112
|
: null,
|
|
107
113
|
|
|
108
|
-
// Input line
|
|
109
|
-
React.createElement(Box, { paddingX: 1 },
|
|
114
|
+
// Input line with char counter
|
|
115
|
+
React.createElement(Box, { paddingX: 1, flexDirection: 'row', alignItems: 'center' },
|
|
110
116
|
React.createElement(Text, { color: 'cyan' }, '> '),
|
|
111
|
-
React.createElement(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
117
|
+
React.createElement(Box, { flexDirection: 'column', flexGrow: 1 },
|
|
118
|
+
React.createElement(TextInput, {
|
|
119
|
+
value,
|
|
120
|
+
onChange: handleChange,
|
|
121
|
+
onSubmit: handleSubmit,
|
|
122
|
+
placeholder: isAtLimit ? '⚠️ limit reached!' : (placeholder || 'type a message...'),
|
|
123
|
+
showCursor: !showCompletions || completions.length <= 1,
|
|
124
|
+
}),
|
|
125
|
+
),
|
|
126
|
+
// Char counter
|
|
127
|
+
React.createElement(Box, { marginLeft: 1 },
|
|
128
|
+
React.createElement(Text, { color: counterColor },
|
|
129
|
+
isAtLimit ? `🚫 ${MAX_CHARS}/${MAX_CHARS}` : `${charCount}/${MAX_CHARS}`
|
|
130
|
+
),
|
|
131
|
+
),
|
|
125
132
|
),
|
|
126
133
|
);
|
|
127
134
|
}
|
package/src/tui/ink-app.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
6
6
|
import { Box, Text, useApp, useInput, useStdin } from 'ink';
|
|
7
7
|
import {
|
|
8
|
-
StatusBar, MessageBubble, ThinkingIndicator,
|
|
8
|
+
AsciiBanner, StatusBar, MessageBubble, ThinkingIndicator, stripEmojis,
|
|
9
9
|
} from './ink-components.js';
|
|
10
10
|
import { CompletableInput } from './completable-input.js';
|
|
11
11
|
import { setInkMode } from '../core/logger.js';
|
|
@@ -30,10 +30,9 @@ export function createApp({ registry, mqtt, stats, session, bus, config, extras
|
|
|
30
30
|
const scrollOffsetRef = useRef(0);
|
|
31
31
|
|
|
32
32
|
// Calculate how many rows are available for messages
|
|
33
|
+
const MAX_VISIBLE_MESSAGES = 12;
|
|
33
34
|
const getAvailableRows = useCallback(() => {
|
|
34
|
-
|
|
35
|
-
// status bar (3) + log line (1) + input box (3) + padding (2) + thinking (1) = ~10
|
|
36
|
-
return Math.max(5, termRows - 10);
|
|
35
|
+
return MAX_VISIBLE_MESSAGES;
|
|
37
36
|
}, []);
|
|
38
37
|
|
|
39
38
|
// ─── Enable Ink mode for logger ──────────────────
|
|
@@ -117,6 +116,23 @@ export function createApp({ registry, mqtt, stats, session, bus, config, extras
|
|
|
117
116
|
}, 500);
|
|
118
117
|
};
|
|
119
118
|
|
|
119
|
+
// Agent progress events
|
|
120
|
+
const onAgentProgress = ({ state, message, tool, action }) => {
|
|
121
|
+
if (state === 'tool') {
|
|
122
|
+
setThinking('tool_call');
|
|
123
|
+
setLogLine(`[TOOL] ${tool}.${action || ''}...`);
|
|
124
|
+
} else if (state === 'tool_done') {
|
|
125
|
+
setThinking('thinking');
|
|
126
|
+
setLogLine(`[TOOL] done, waiting...`);
|
|
127
|
+
} else if (state === 'receiving') {
|
|
128
|
+
setLogLine(`[LLM] ${message}`);
|
|
129
|
+
} else if (state === 'thinking') {
|
|
130
|
+
setThinking('thinking');
|
|
131
|
+
} else if (state === 'timeout') {
|
|
132
|
+
setLogLine(`[WARN] ${message}`);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
120
136
|
bus.on('llm:text', onLlmText);
|
|
121
137
|
bus.on('tts:sentence', onTtsSentence);
|
|
122
138
|
bus.on('tool:called', onToolCalled);
|
|
@@ -124,6 +140,7 @@ export function createApp({ registry, mqtt, stats, session, bus, config, extras
|
|
|
124
140
|
bus.on('ready', onReady);
|
|
125
141
|
bus.on('mqtt:error', onDisconnect);
|
|
126
142
|
bus.on('log:message', onLogMessage);
|
|
143
|
+
bus.on('agent:progress', onAgentProgress);
|
|
127
144
|
|
|
128
145
|
return () => {
|
|
129
146
|
bus.off('llm:text', onLlmText);
|
|
@@ -133,6 +150,7 @@ export function createApp({ registry, mqtt, stats, session, bus, config, extras
|
|
|
133
150
|
bus.off('ready', onReady);
|
|
134
151
|
bus.off('mqtt:error', onDisconnect);
|
|
135
152
|
bus.off('log:message', onLogMessage);
|
|
153
|
+
bus.off('agent:progress', onAgentProgress);
|
|
136
154
|
if (ttsTimer) clearTimeout(ttsTimer);
|
|
137
155
|
};
|
|
138
156
|
}, []);
|
|
@@ -171,6 +189,16 @@ export function createApp({ registry, mqtt, stats, session, bus, config, extras
|
|
|
171
189
|
|
|
172
190
|
// ─── Handle Submit ────────────────────────────────
|
|
173
191
|
const handleSubmit = useCallback(async (text) => {
|
|
192
|
+
// Limit input to 70 chars (XiaoZhi limit)
|
|
193
|
+
const MAX_INPUT = 70;
|
|
194
|
+
if (text.length > MAX_INPUT) {
|
|
195
|
+
setMessages(prev => [...prev.slice(-maxMessages), {
|
|
196
|
+
id: `warn-${Date.now()}`, role: 'system',
|
|
197
|
+
content: `⚠️ Input too long (${text.length}/${MAX_INPUT} chars)`,
|
|
198
|
+
}]);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
174
202
|
// Add user message
|
|
175
203
|
setMessages(prev => [...prev.slice(-maxMessages), {
|
|
176
204
|
id: `user-${Date.now()}`, role: 'user', content: text,
|
|
@@ -244,31 +272,82 @@ export function createApp({ registry, mqtt, stats, session, bus, config, extras
|
|
|
244
272
|
}
|
|
245
273
|
} catch {}
|
|
246
274
|
|
|
247
|
-
// Send to XiaoZhi
|
|
275
|
+
// Send to XiaoZhi via Agent Controller (if available) or direct
|
|
248
276
|
setThinking('thinking');
|
|
249
|
-
|
|
277
|
+
|
|
278
|
+
const agentController = extras?.agentController;
|
|
279
|
+
if (agentController) {
|
|
280
|
+
// Reset agent if it's stuck in running state
|
|
281
|
+
if (agentController.isRunning) {
|
|
282
|
+
agentController.abort();
|
|
283
|
+
await new Promise(r => setTimeout(r, 500));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Use agent controller for proper response handling
|
|
287
|
+
try {
|
|
288
|
+
const result = await agentController.run(text, { generatePlan: false });
|
|
289
|
+
setThinking('idle');
|
|
290
|
+
|
|
291
|
+
if (result.success && result.result) {
|
|
292
|
+
setMessages(prev => [...prev.slice(-maxMessages), {
|
|
293
|
+
id: `assistant-${Date.now()}`, role: 'assistant', content: result.result,
|
|
294
|
+
}]);
|
|
295
|
+
}
|
|
296
|
+
} catch (err) {
|
|
297
|
+
setThinking('idle');
|
|
298
|
+
setMessages(prev => [...prev.slice(-maxMessages), {
|
|
299
|
+
id: `error-${Date.now()}`, role: 'system', content: `Error: ${err.message}`,
|
|
300
|
+
}]);
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
// Fallback: send directly to XiaoZhi
|
|
304
|
+
mqtt.sendText(text);
|
|
305
|
+
}
|
|
250
306
|
}, [registry, mqtt, stats, session]);
|
|
251
307
|
|
|
252
308
|
// ─── Render ───────────────────────────────────────
|
|
253
309
|
const tools = registry.all();
|
|
254
310
|
const actions = tools.reduce((s, t) => s + (t.actions?.length || 0), 0);
|
|
255
311
|
|
|
256
|
-
// Calculate visible messages (scrollable window)
|
|
257
|
-
const
|
|
312
|
+
// Calculate visible messages (scrollable window, max 12 rendered lines)
|
|
313
|
+
const MAX_RENDER_LINES = 12;
|
|
258
314
|
const totalMessages = messages.length;
|
|
259
315
|
const endIdx = totalMessages - scrollOffset;
|
|
260
|
-
|
|
316
|
+
|
|
317
|
+
// Count lines backwards from endIdx to fit within MAX_RENDER_LINES
|
|
318
|
+
let usedLines = 0;
|
|
319
|
+
let startIdx = endIdx;
|
|
320
|
+
for (let i = endIdx - 1; i >= 0; i--) {
|
|
321
|
+
const msg = messages[i];
|
|
322
|
+
let msgLines = 1;
|
|
323
|
+
if (msg.role === 'assistant' && msg.content) {
|
|
324
|
+
const w = (process.stdout.columns || 80) - 6;
|
|
325
|
+
const words = stripEmojis(msg.content).split(/\s+/);
|
|
326
|
+
let lineLen = 0;
|
|
327
|
+
msgLines = 1;
|
|
328
|
+
for (const word of words) {
|
|
329
|
+
if (lineLen + word.length + 1 > w && lineLen > 0) { msgLines++; lineLen = word.length; }
|
|
330
|
+
else { lineLen += word.length + (lineLen > 0 ? 1 : 0); }
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (usedLines + msgLines > MAX_RENDER_LINES) break;
|
|
334
|
+
usedLines += msgLines;
|
|
335
|
+
startIdx = i;
|
|
336
|
+
}
|
|
337
|
+
|
|
261
338
|
const visibleMessages = messages.slice(startIdx, endIdx);
|
|
262
|
-
const canScrollUp =
|
|
339
|
+
const canScrollUp = startIdx > 0;
|
|
263
340
|
const canScrollDown = scrollOffset > 0;
|
|
264
341
|
|
|
265
342
|
return React.createElement(Box, {
|
|
266
343
|
flexDirection: 'column', width: '100%',
|
|
267
344
|
},
|
|
268
|
-
// Top: Status bar
|
|
345
|
+
// Top: Banner + Status bar
|
|
346
|
+
React.createElement(AsciiBanner, {
|
|
347
|
+
version: config?.agent?.version,
|
|
348
|
+
}),
|
|
269
349
|
React.createElement(StatusBar, {
|
|
270
350
|
connected, session: sessionId,
|
|
271
|
-
version: config?.agent?.version,
|
|
272
351
|
}),
|
|
273
352
|
|
|
274
353
|
// Log status line (single line, auto-updating, auto-hiding)
|
|
@@ -5,25 +5,72 @@
|
|
|
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
|
+
export const AsciiBanner = React.memo(function AsciiBanner({ version }) {
|
|
29
|
+
const v = version || '0.0.0';
|
|
30
|
+
return React.createElement(Box, {
|
|
31
|
+
flexDirection: 'column', paddingLeft: 1,
|
|
32
|
+
},
|
|
33
|
+
React.createElement(Box, null,
|
|
34
|
+
React.createElement(Text, { color: 'cyan' }, ' /\\_/\\ '),
|
|
35
|
+
React.createElement(Text, { color: 'cyan', bold: true }, 'PELULU-CLI'),
|
|
36
|
+
),
|
|
37
|
+
React.createElement(Text, { color: 'cyan' }, ' ( o.o ) v' + v),
|
|
38
|
+
React.createElement(Text, { color: 'cyan' }, ' > ^ < the tiny coding agent'),
|
|
39
|
+
React.createElement(Text, { color: 'cyan' }, ' /| |\\ powered by XiaoZhi'),
|
|
40
|
+
React.createElement(Text, { color: 'cyan' }, '(_| |_)'),
|
|
41
|
+
React.createElement(Text, { dimColor: true }, ' 18 tools • MCP protocol • agent mode'),
|
|
42
|
+
);
|
|
43
|
+
});
|
|
8
44
|
|
|
9
45
|
// ─── Status Bar ───────────────────────────────────────────
|
|
10
|
-
|
|
46
|
+
// Memoized — only re-renders when connected/session changes
|
|
47
|
+
export const StatusBar = React.memo(function StatusBar({ connected, session }) {
|
|
48
|
+
const statusDot = connected ? '●' : '○';
|
|
49
|
+
const statusColor = connected ? 'green' : 'red';
|
|
50
|
+
const sess = session ? session.slice(0, 8) : '---';
|
|
51
|
+
|
|
11
52
|
return React.createElement(Box, {
|
|
12
|
-
borderStyle: 'single', borderColor: 'cyan', width: '100%',
|
|
53
|
+
borderStyle: 'single', borderColor: 'cyan', width: '100%',
|
|
54
|
+
paddingX: 1, paddingY: 0,
|
|
55
|
+
flexDirection: 'row', justifyContent: 'space-between',
|
|
13
56
|
},
|
|
14
|
-
React.createElement(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
57
|
+
React.createElement(Box, { flexDirection: 'row' },
|
|
58
|
+
React.createElement(Text, { color: 'cyan', bold: true }, '🐱 PELULU '),
|
|
59
|
+
React.createElement(Text, { color: statusColor }, statusDot),
|
|
60
|
+
React.createElement(Text, { color: statusColor, bold: connected },
|
|
61
|
+
connected ? ' online' : ' offline'
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
React.createElement(Box, { flexDirection: 'row' },
|
|
65
|
+
React.createElement(Text, { dimColor: true }, `session:${sess}`),
|
|
66
|
+
React.createElement(Text, { dimColor: true }, ' '),
|
|
67
|
+
React.createElement(Text, { dimColor: true }, 'xiaozhi.me'),
|
|
18
68
|
),
|
|
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
69
|
);
|
|
23
|
-
}
|
|
70
|
+
});
|
|
24
71
|
|
|
25
72
|
// ─── Strip Emojis ─────────────────────────────────────────
|
|
26
|
-
function stripEmojis(text) {
|
|
73
|
+
export function stripEmojis(text) {
|
|
27
74
|
return text
|
|
28
75
|
.replace(/\p{Emoji_Presentation}/gu, '')
|
|
29
76
|
.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
|
|