pelulu-cli 1.0.0 → 1.0.1
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/DailyNotesApp/app/build.gradle.kts +57 -0
- package/DailyNotesApp/app/src/main/java/com/dailynotes/app/MainActivity.kt +48 -0
- package/DailyNotesApp/app/src/main/java/com/dailynotes/app/Note.kt +13 -0
- package/DailyNotesApp/app/src/main/java/com/dailynotes/app/NoteDatabase.kt +9 -0
- package/DailyNotesApp/build.gradle.kts +5 -0
- package/DailyNotesApp/settings.gradle.kts +17 -0
- package/README.md +69 -82
- package/config.example.json +2 -1
- package/package.json +6 -3
- package/preview.jpg +0 -0
- package/src/core/auto-format.js +61 -12
- package/src/core/config.js +1 -1
- package/src/core/confirm.js +2 -2
- package/src/core/diff.js +4 -4
- package/src/core/doctor.js +2 -2
- package/src/core/error-handler.js +1 -1
- package/src/core/file-tracker.js +4 -4
- package/src/core/formatter.js +19 -19
- package/src/core/logger.js +19 -3
- package/src/core/spinner.js +2 -2
- package/src/core/stats.js +2 -2
- package/src/core/thinking.js +8 -8
- package/src/core/tool-help.js +2 -2
- package/src/core/tool-registry.js +6 -10
- package/src/core/update-checker.js +125 -0
- package/src/core/wizard.js +5 -5
- package/src/core/workspace.js +4 -4
- package/src/index.js +82 -37
- package/src/mcp/activation.js +3 -3
- package/src/mcp/message-sender.js +1 -1
- package/src/mcp/mqtt-client.js +4 -4
- package/src/repl-commands.js +2 -1
- package/src/repl.js +26 -6
- package/src/tools/config.js +1 -1
- package/src/tools/diff.js +6 -0
- package/src/tools/file.js +3 -3
- package/src/tools/git.js +1 -1
- package/src/tools/network.js +1 -1
- package/src/tools/project.js +2 -2
- package/src/tools/search.js +3 -3
- package/src/tools/shell.js +2 -2
- package/src/tools/template.js +1 -1
- package/src/tools/watch.js +1 -1
- package/src/tui/completable-input.js +127 -0
- package/src/tui/ink-app.js +324 -0
- package/src/tui/ink-components.js +157 -0
- package/src/tui/ink-entry.js +86 -0
- package/src/tui/renderer.js +137 -19
- package/src/tui/status-bar.js +7 -7
package/src/core/formatter.js
CHANGED
|
@@ -25,35 +25,35 @@ function tryParse(text) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function formatError(text) {
|
|
28
|
-
return `${COLORS.red}
|
|
28
|
+
return `${COLORS.red}[ERR] ${text}${COLORS.reset}`;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function formatFileResult(action, data) {
|
|
32
32
|
if (action === 'read') {
|
|
33
33
|
const lines = data.content?.split('\n').length || 0;
|
|
34
|
-
return `${COLORS.green}
|
|
34
|
+
return `${COLORS.green}[OK]${COLORS.reset} ${data.path} (${lines} lines, ${data.totalSize} chars)`;
|
|
35
35
|
}
|
|
36
|
-
if (action === 'write') return `${COLORS.green}
|
|
37
|
-
if (action === 'edit') return `${COLORS.green}
|
|
36
|
+
if (action === 'write') return `${COLORS.green}[OK]${COLORS.reset} Written: ${data.path} (${data.written} chars)`;
|
|
37
|
+
if (action === 'edit') return `${COLORS.green}[OK]${COLORS.reset} Edited: ${data.path} (${data.occurrences} occurrence(s))`;
|
|
38
38
|
if (action === 'list') {
|
|
39
39
|
const dirs = data.items?.filter(i => i.type === 'dir').length || 0;
|
|
40
40
|
const files = data.items?.filter(i => i.type === 'file').length || 0;
|
|
41
|
-
return `${COLORS.green}
|
|
41
|
+
return `${COLORS.green}[OK]${COLORS.reset} ${data.path}: ${dirs} dirs, ${files} files`;
|
|
42
42
|
}
|
|
43
|
-
if (action === 'exists') return data.exists ?
|
|
44
|
-
if (action === 'delete') return `${COLORS.yellow}
|
|
45
|
-
return `${COLORS.green}
|
|
43
|
+
if (action === 'exists') return data.exists ? `[OK] ${data.type}: ${data.path}` : `[ERR] Not found: ${data.path}`;
|
|
44
|
+
if (action === 'delete') return `${COLORS.yellow}[DEL]${COLORS.reset} Deleted: ${data.path}`;
|
|
45
|
+
return `${COLORS.green}[OK]${COLORS.reset} ${action}: ${JSON.stringify(data)}`;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function formatGitResult(action, data) {
|
|
49
49
|
if (action === 'status') {
|
|
50
50
|
const dirty = data.dirty ? `${COLORS.yellow}dirty${COLORS.reset}` : `${COLORS.green}clean${COLORS.reset}`;
|
|
51
|
-
return
|
|
51
|
+
return `${data.branch} (${dirty}) ${data.changes?.length || 0} changes`;
|
|
52
52
|
}
|
|
53
|
-
if (action === 'log') return
|
|
54
|
-
if (action === 'commit') return data.committed ? `${COLORS.green}
|
|
55
|
-
if (action === 'branch') return data.branches ?
|
|
56
|
-
return `${COLORS.green}
|
|
53
|
+
if (action === 'log') return `${data.commits?.length || 0} commits:\n ${data.commits?.join('\n ')}`;
|
|
54
|
+
if (action === 'commit') return data.committed ? `${COLORS.green}[OK]${COLORS.reset} Committed: ${data.message}` : `[WARN] ${data.reason}`;
|
|
55
|
+
if (action === 'branch') return data.branches ? `Branches:\n ${data.branches.join('\n ')}` : `[OK] ${data.created || data.deleted}`;
|
|
56
|
+
return `${COLORS.green}[OK]${COLORS.reset} git ${action}: ${JSON.stringify(data)}`;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function formatShellResult(data) {
|
|
@@ -65,16 +65,16 @@ function formatShellResult(data) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
function formatSearchResult(action, data) {
|
|
68
|
-
if (action === 'grep') return
|
|
69
|
-
if (action === 'find') return
|
|
70
|
-
if (action === 'web') return
|
|
68
|
+
if (action === 'grep') return `${data.matches} matches for "${data.pattern}":\n ${(data.results || []).join('\n ')}`;
|
|
69
|
+
if (action === 'find') return `${data.matches} files matching "${data.pattern}":\n ${(data.files || []).join('\n ')}`;
|
|
70
|
+
if (action === 'web') return `${data.url} (${data.status}):\n${data.body?.slice(0, 500)}`;
|
|
71
71
|
return JSON.stringify(data, null, 2);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
function formatProjectResult(action, data) {
|
|
75
|
-
if (action === 'build') return data.success ? `${COLORS.green}
|
|
76
|
-
if (action === 'test') return data.passed ? `${COLORS.green}
|
|
77
|
-
if (action === 'info') return
|
|
75
|
+
if (action === 'build') return data.success ? `${COLORS.green}[OK] Build OK${COLORS.reset} (${data.type})` : `${COLORS.red}[ERR] Build failed${COLORS.reset} (exit ${data.exitCode})`;
|
|
76
|
+
if (action === 'test') return data.passed ? `${COLORS.green}[OK] Tests passed${COLORS.reset}` : `${COLORS.red}[ERR] Tests failed${COLORS.reset} (exit ${data.exitCode})`;
|
|
77
|
+
if (action === 'info') return `${data.name || data.type}: ${data.scripts?.join(', ') || 'no scripts'}`;
|
|
78
78
|
return JSON.stringify(data, null, 2);
|
|
79
79
|
}
|
|
80
80
|
|
package/src/core/logger.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Logger — colored terminal output with levels
|
|
3
|
+
* Supports Ink mode: when active, logs go through bus instead of console
|
|
3
4
|
*/
|
|
4
5
|
let _debug = false;
|
|
6
|
+
let _inkMode = false;
|
|
7
|
+
let _bus = null;
|
|
5
8
|
|
|
6
9
|
export const COLORS = {
|
|
7
10
|
reset: '\x1b[0m',
|
|
@@ -16,10 +19,16 @@ export const COLORS = {
|
|
|
16
19
|
gray: '\x1b[90m',
|
|
17
20
|
};
|
|
18
21
|
|
|
22
|
+
export function setInkMode(enabled, bus) {
|
|
23
|
+
_inkMode = enabled;
|
|
24
|
+
_bus = bus;
|
|
25
|
+
}
|
|
26
|
+
export function isInkMode() { return _inkMode; }
|
|
27
|
+
|
|
19
28
|
const ICONS = {
|
|
20
|
-
ok: '
|
|
21
|
-
tool: '
|
|
22
|
-
user: '
|
|
29
|
+
ok: '[OK]', err: '[ERR]', warn: '[WARN]', info: '[i]',
|
|
30
|
+
tool: '[TOOL]', mcp: '[MCP]', plugin: '[PLUG]', system: '[SYS]',
|
|
31
|
+
user: '[USER]', ai: '[AI]', debug: '[DBG]', file: '[FILE]',
|
|
23
32
|
};
|
|
24
33
|
|
|
25
34
|
export function setDebug(enabled) { _debug = enabled; }
|
|
@@ -28,6 +37,13 @@ export function debug(msg, data) { log('debug', msg, data); }
|
|
|
28
37
|
|
|
29
38
|
export function log(level, msg, data) {
|
|
30
39
|
if (level === 'debug' && !_debug) return;
|
|
40
|
+
|
|
41
|
+
// In Ink mode, route logs through bus so they render inside the TUI
|
|
42
|
+
if (_inkMode && _bus) {
|
|
43
|
+
_bus.emit('log:message', { level, msg, data });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
const icon = ICONS[level] || '';
|
|
32
48
|
const color = {
|
|
33
49
|
ok: COLORS.green, err: COLORS.red, warn: COLORS.yellow,
|
package/src/core/spinner.js
CHANGED
|
@@ -39,11 +39,11 @@ export class Spinner {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
success(text) {
|
|
42
|
-
this.stop(`${COLORS.green}
|
|
42
|
+
this.stop(`${COLORS.green}[OK] ${text}${COLORS.reset}`);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
fail(text) {
|
|
46
|
-
this.stop(`${COLORS.red}
|
|
46
|
+
this.stop(`${COLORS.red}[ERR] ${text}${COLORS.reset}`);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
package/src/core/stats.js
CHANGED
|
@@ -41,9 +41,9 @@ export class Stats {
|
|
|
41
41
|
formatReport() {
|
|
42
42
|
const s = this.getSummary();
|
|
43
43
|
const lines = [
|
|
44
|
-
|
|
44
|
+
`[STATS] Session Stats`,
|
|
45
45
|
` Uptime: ${s.uptime}s`,
|
|
46
|
-
` Tool calls: ${s.total} (${s.success}
|
|
46
|
+
` Tool calls: ${s.total} (${s.success} [OK] / ${s.failed} [ERR])`,
|
|
47
47
|
` Avg duration: ${s.avgDuration}ms`,
|
|
48
48
|
];
|
|
49
49
|
if (s.topTools.length) {
|
package/src/core/thinking.js
CHANGED
|
@@ -6,14 +6,14 @@ import { COLORS } from './logger.js';
|
|
|
6
6
|
import { bus } from './event-bus.js';
|
|
7
7
|
|
|
8
8
|
const THINK_STATES = {
|
|
9
|
-
idle: { icon: '
|
|
10
|
-
thinking: { icon: '
|
|
11
|
-
tool_call: { icon: '
|
|
12
|
-
reading: { icon: '
|
|
13
|
-
writing: { icon: '
|
|
14
|
-
searching: { icon: '
|
|
15
|
-
building: { icon: '
|
|
16
|
-
testing: { icon: '
|
|
9
|
+
idle: { icon: '-', text: 'Idle' },
|
|
10
|
+
thinking: { icon: '..', text: 'Thinking...' },
|
|
11
|
+
tool_call: { icon: '>>', text: 'Using tool...' },
|
|
12
|
+
reading: { icon: '..', text: 'Reading file...' },
|
|
13
|
+
writing: { icon: '..', text: 'Writing...' },
|
|
14
|
+
searching: { icon: '..', text: 'Searching...' },
|
|
15
|
+
building: { icon: '..', text: 'Building...' },
|
|
16
|
+
testing: { icon: '..', text: 'Testing...' },
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
export class Thinking {
|
package/src/core/tool-help.js
CHANGED
|
@@ -99,7 +99,7 @@ export function showToolHelp(toolName) {
|
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
console.log(`\n${COLORS.bold}
|
|
102
|
+
console.log(`\n${COLORS.bold}[DOC] ${toolName} — Examples:${COLORS.reset}\n`);
|
|
103
103
|
for (const ex of examples) {
|
|
104
104
|
console.log(` ${COLORS.cyan}${ex}${COLORS.reset}`);
|
|
105
105
|
}
|
|
@@ -108,7 +108,7 @@ export function showToolHelp(toolName) {
|
|
|
108
108
|
|
|
109
109
|
export function showAllToolHelp() {
|
|
110
110
|
for (const [tool, examples] of Object.entries(TOOL_EXAMPLES)) {
|
|
111
|
-
console.log(`\n${COLORS.bold}
|
|
111
|
+
console.log(`\n${COLORS.bold}[DOC] ${tool}:${COLORS.reset}`);
|
|
112
112
|
for (const ex of examples.slice(0, 2)) {
|
|
113
113
|
console.log(` ${COLORS.cyan}${ex}${COLORS.reset}`);
|
|
114
114
|
}
|
|
@@ -18,23 +18,19 @@ export class ToolRegistry {
|
|
|
18
18
|
#tools = new Map();
|
|
19
19
|
|
|
20
20
|
async loadBuiltins() {
|
|
21
|
+
const failed = [];
|
|
21
22
|
try {
|
|
22
23
|
const files = await readdir(TOOLS_DIR);
|
|
23
24
|
for (const file of files.filter(f => f.endsWith('.js'))) {
|
|
24
25
|
try {
|
|
25
26
|
const mod = await import(join(TOOLS_DIR, file));
|
|
26
27
|
const tool = mod.default || mod;
|
|
27
|
-
if (tool?.name && tool?.handler)
|
|
28
|
-
|
|
29
|
-
log('tool', ` ${tool.name} — ${tool.description || ''} (${tool.actions?.length || 0} actions)`);
|
|
30
|
-
}
|
|
31
|
-
} catch (e) {
|
|
32
|
-
log('warn', `Tool "${file}" failed: ${e.message}`);
|
|
33
|
-
}
|
|
28
|
+
if (tool?.name && tool?.handler) this.register(tool);
|
|
29
|
+
} catch (e) { failed.push(file); }
|
|
34
30
|
}
|
|
35
|
-
} catch (e) {
|
|
36
|
-
|
|
37
|
-
}
|
|
31
|
+
} catch (e) { /* silent */ }
|
|
32
|
+
if (failed.length) log('warn', `Failed tools: ${failed.join(', ')}`);
|
|
33
|
+
return { loaded: this.#tools.size, failed: failed.length };
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
register(tool) {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Checker — compare local version with GitHub releases
|
|
3
|
+
* Uses: https://api.github.com/repos/venenapro/Pelulu-CLI/releases
|
|
4
|
+
*/
|
|
5
|
+
import { readFile } from 'fs/promises';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
|
|
8
|
+
const GITHUB_RELEASES_URL = 'https://api.github.com/repos/venenapro/Pelulu-CLI/releases';
|
|
9
|
+
const TIMEOUT_MS = 8000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse semver string "1.2.3" → { major, minor, patch }
|
|
13
|
+
*/
|
|
14
|
+
function parseSemver(version) {
|
|
15
|
+
const clean = version.replace(/^v/i, '');
|
|
16
|
+
const match = clean.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
17
|
+
if (!match) return null;
|
|
18
|
+
return { major: +match[1], minor: +match[2], patch: +match[3] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compare two semver objects.
|
|
23
|
+
* Returns: 1 if a > b, -1 if a < b, 0 if equal
|
|
24
|
+
*/
|
|
25
|
+
function compareSemver(a, b) {
|
|
26
|
+
if (a.major !== b.major) return a.major > b.major ? 1 : -1;
|
|
27
|
+
if (a.minor !== b.minor) return a.minor > b.minor ? 1 : -1;
|
|
28
|
+
if (a.patch !== b.patch) return a.patch > b.patch ? 1 : -1;
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read local version from package.json
|
|
34
|
+
*/
|
|
35
|
+
async function getLocalVersion(root) {
|
|
36
|
+
try {
|
|
37
|
+
const raw = await readFile(join(root, 'package.json'), 'utf-8');
|
|
38
|
+
const pkg = JSON.parse(raw);
|
|
39
|
+
return pkg.version || null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fetch latest release from GitHub API.
|
|
47
|
+
* Returns { tag, name, body, url, publishedAt } or null on failure.
|
|
48
|
+
*/
|
|
49
|
+
async function fetchLatestRelease() {
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(GITHUB_RELEASES_URL, {
|
|
55
|
+
signal: controller.signal,
|
|
56
|
+
headers: {
|
|
57
|
+
'Accept': 'application/vnd.github+json',
|
|
58
|
+
'User-Agent': 'Pelulu-CLI',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!res.ok) return null;
|
|
63
|
+
|
|
64
|
+
const releases = await res.json();
|
|
65
|
+
if (!Array.isArray(releases) || releases.length === 0) return null;
|
|
66
|
+
|
|
67
|
+
// Filter out prerelease/draft, pick latest stable
|
|
68
|
+
const stable = releases.find(r => !r.prerelease && !r.draft) || releases[0];
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
tag: stable.tag_name || '',
|
|
72
|
+
name: stable.name || stable.tag_name || '',
|
|
73
|
+
body: stable.body || '',
|
|
74
|
+
url: stable.html_url || '',
|
|
75
|
+
publishedAt: stable.published_at || '',
|
|
76
|
+
};
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
} finally {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check for updates.
|
|
86
|
+
* Returns:
|
|
87
|
+
* { available: true, local, remote, release } — update available
|
|
88
|
+
* { available: false, local, remote } — up to date
|
|
89
|
+
* { available: false, error: true, message } — check failed
|
|
90
|
+
*/
|
|
91
|
+
export async function checkForUpdates(root) {
|
|
92
|
+
const localVersion = await getLocalVersion(root);
|
|
93
|
+
if (!localVersion) {
|
|
94
|
+
return { available: false, error: true, message: 'Could not read local version from package.json' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const release = await fetchLatestRelease();
|
|
98
|
+
if (!release) {
|
|
99
|
+
return { available: false, error: true, local: localVersion, message: 'Could not fetch GitHub releases' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const localParsed = parseSemver(localVersion);
|
|
103
|
+
const remoteParsed = parseSemver(release.tag);
|
|
104
|
+
|
|
105
|
+
if (!localParsed || !remoteParsed) {
|
|
106
|
+
return { available: false, error: true, local: localVersion, remote: release.tag, message: 'Invalid version format' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const cmp = compareSemver(remoteParsed, localParsed);
|
|
110
|
+
|
|
111
|
+
if (cmp > 0) {
|
|
112
|
+
return {
|
|
113
|
+
available: true,
|
|
114
|
+
local: localVersion,
|
|
115
|
+
remote: release.tag.replace(/^v/i, ''),
|
|
116
|
+
release,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
available: false,
|
|
122
|
+
local: localVersion,
|
|
123
|
+
remote: release.tag.replace(/^v/i, ''),
|
|
124
|
+
};
|
|
125
|
+
}
|
package/src/core/wizard.js
CHANGED
|
@@ -12,7 +12,7 @@ export async function runWizard(root) {
|
|
|
12
12
|
if (config._wizard_done) return config;
|
|
13
13
|
|
|
14
14
|
console.log(`\n${COLORS.cyan}╔══════════════════════════════════════╗`);
|
|
15
|
-
console.log(`║
|
|
15
|
+
console.log(`║ Pelulu CLI Setup ║`);
|
|
16
16
|
console.log(`╚══════════════════════════════════════╝${COLORS.reset}\n`);
|
|
17
17
|
|
|
18
18
|
if (!process.stdin.isTTY) {
|
|
@@ -31,23 +31,23 @@ export async function runWizard(root) {
|
|
|
31
31
|
|
|
32
32
|
try {
|
|
33
33
|
// Workspace
|
|
34
|
-
const workspace = await ask('
|
|
34
|
+
const workspace = await ask('[DIR] Workspace path?', config.agent?.workspace || '~/coding-agent');
|
|
35
35
|
config.agent = { ...config.agent, workspace };
|
|
36
36
|
|
|
37
37
|
// MCP endpoint (optional)
|
|
38
38
|
console.log(`\n${COLORS.dim}MCP Endpoint (optional, for official XiaoZhi MCP):${COLORS.reset}`);
|
|
39
39
|
console.log(`${COLORS.dim}Get from: xiaozhi.me → Configure → Extensions → MCP Endpoint${COLORS.reset}`);
|
|
40
|
-
const mcpUrl = await ask('
|
|
40
|
+
const mcpUrl = await ask('[MCP] MCP Endpoint URL?', '');
|
|
41
41
|
if (mcpUrl) config.mcp = { ...config.mcp, endpoint_url: mcpUrl };
|
|
42
42
|
|
|
43
43
|
// Shell timeout
|
|
44
|
-
const timeout = await ask('
|
|
44
|
+
const timeout = await ask('Shell timeout (ms)?', String(config.tools?.shell_timeout || 30000));
|
|
45
45
|
config.tools = { ...config.tools, shell_timeout: parseInt(timeout) || 30000 };
|
|
46
46
|
|
|
47
47
|
config._wizard_done = true;
|
|
48
48
|
await saveConfig(root, config);
|
|
49
49
|
|
|
50
|
-
console.log(`\n${COLORS.green}
|
|
50
|
+
console.log(`\n${COLORS.green}[OK] Configuration saved!${COLORS.reset}\n`);
|
|
51
51
|
return config;
|
|
52
52
|
} finally {
|
|
53
53
|
rl.close();
|
package/src/core/workspace.js
CHANGED
|
@@ -82,15 +82,15 @@ export async function detectWorkspace(dir) {
|
|
|
82
82
|
|
|
83
83
|
export function formatWorkspace(info) {
|
|
84
84
|
const lines = [
|
|
85
|
-
|
|
85
|
+
`[DIR] ${info.name} (${info.type})`,
|
|
86
86
|
` Language: ${info.language}`,
|
|
87
87
|
` Path: ${info.path}`,
|
|
88
88
|
];
|
|
89
89
|
if (info.version) lines.push(` Version: ${info.version}`);
|
|
90
90
|
if (info.scripts?.length) lines.push(` Scripts: ${info.scripts.join(', ')}`);
|
|
91
91
|
if (info.dirs?.length) lines.push(` Dirs: ${info.dirs.join(', ')}`);
|
|
92
|
-
if (info.hasGit) lines.push(' Git:
|
|
93
|
-
if (info.hasDocker) lines.push(' Docker:
|
|
94
|
-
if (info.hasCI) lines.push(' CI:
|
|
92
|
+
if (info.hasGit) lines.push(' Git: [OK]');
|
|
93
|
+
if (info.hasDocker) lines.push(' Docker: [OK]');
|
|
94
|
+
if (info.hasCI) lines.push(' CI: [OK]');
|
|
95
95
|
return lines.join('\n');
|
|
96
96
|
}
|
package/src/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Pelulu CLI — Entry Point
|
|
4
|
+
* Ink-based TUI with React components
|
|
5
5
|
*/
|
|
6
6
|
import { dirname, join } from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
|
+
import chalk from 'chalk';
|
|
8
9
|
import { loadConfig, saveConfig } from './core/config.js';
|
|
9
10
|
import { log, setDebug } from './core/logger.js';
|
|
10
11
|
import { bus } from './core/event-bus.js';
|
|
@@ -16,44 +17,70 @@ import { buildSystemPrompt } from './core/system-prompt.js';
|
|
|
16
17
|
import { buildContext } from './core/context.js';
|
|
17
18
|
import { isDestructive, askConfirmation } from './core/confirm.js';
|
|
18
19
|
import { runWizard } from './core/wizard.js';
|
|
20
|
+
import { handleError } from './core/error-handler.js';
|
|
21
|
+
import { withRetry } from './core/retry.js';
|
|
22
|
+
import { withSpinner } from './core/spinner.js';
|
|
23
|
+
import { Thinking } from './core/thinking.js';
|
|
24
|
+
import { autoFormat } from './core/auto-format.js';
|
|
25
|
+
import { FileTracker } from './core/file-tracker.js';
|
|
19
26
|
import { MqttClient } from './mcp/mqtt-client.js';
|
|
27
|
+
import { MessageSender } from './mcp/message-sender.js';
|
|
20
28
|
import { WssEndpoint } from './mcp/wss-endpoint.js';
|
|
21
29
|
import { PluginManager } from './plugins/manager.js';
|
|
22
|
-
import {
|
|
30
|
+
import { checkForUpdates } from './core/update-checker.js';
|
|
31
|
+
import { renderUpdateNotification, renderAsciiBanner } from './tui/renderer.js';
|
|
32
|
+
import { startInkTUI } from './tui/ink-entry.js';
|
|
23
33
|
|
|
24
34
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
35
|
const ROOT = join(__dirname, '..');
|
|
26
36
|
|
|
27
37
|
const args = process.argv.slice(2);
|
|
28
38
|
if (args.includes('--debug')) setDebug(true);
|
|
29
|
-
if (args.includes('--wizard')) { /* force wizard */ }
|
|
30
39
|
const LIST_TOOLS = args.includes('--list-tools');
|
|
31
40
|
|
|
32
41
|
async function main() {
|
|
33
|
-
//
|
|
42
|
+
// Buffer ALL startup logs for Ink to display inside TUI
|
|
43
|
+
const startupLogs = [];
|
|
44
|
+
const origLog = console.log;
|
|
45
|
+
const bufferLog = (...args) => startupLogs.push(args.join(' '));
|
|
46
|
+
|
|
47
|
+
// 1. Load config
|
|
34
48
|
const config = await loadConfig(ROOT);
|
|
35
49
|
|
|
36
|
-
// 2.
|
|
50
|
+
// 2. Check for updates — block if outdated
|
|
51
|
+
const update = await checkForUpdates(ROOT);
|
|
52
|
+
if (update.available) {
|
|
53
|
+
renderUpdateNotification(update);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 3. Workspace & wizard
|
|
37
58
|
const cwd = process.cwd();
|
|
38
59
|
config.agent = { ...config.agent, workspace: cwd };
|
|
39
|
-
log('info', `Workspace: ${cwd}`);
|
|
40
|
-
|
|
41
60
|
if (!config._wizard_done || args.includes('--wizard')) {
|
|
42
61
|
await runWizard(ROOT);
|
|
43
62
|
}
|
|
44
63
|
|
|
45
|
-
//
|
|
64
|
+
// Show ASCII banner before Ink takes over
|
|
65
|
+
renderAsciiBanner(config.agent?.version);
|
|
66
|
+
|
|
67
|
+
// Redirect console.log to buffer — everything after this goes into Ink
|
|
68
|
+
console.log = bufferLog;
|
|
69
|
+
|
|
70
|
+
// 4. Load tools
|
|
46
71
|
const registry = new ToolRegistry();
|
|
47
|
-
|
|
48
|
-
await registry.loadBuiltins();
|
|
72
|
+
const pluginMgr = new PluginManager(registry);
|
|
49
73
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
74
|
+
const { loaded } = await withSpinner('Loading tools...', async () => {
|
|
75
|
+
return await registry.loadBuiltins();
|
|
76
|
+
});
|
|
77
|
+
await pluginMgr.load();
|
|
78
|
+
const actions = registry.all().reduce((s, t) => s + (t.actions?.length || 0), 0);
|
|
53
79
|
|
|
54
80
|
if (LIST_TOOLS) {
|
|
81
|
+
console.log = origLog;
|
|
55
82
|
const tools = registry.list();
|
|
56
|
-
console.log('\
|
|
83
|
+
console.log('\nTools:\n');
|
|
57
84
|
for (const t of tools) console.log(` ${t.name} — ${t.description} (${t.actions.join(', ')})`);
|
|
58
85
|
console.log(`\n ${tools.length} tools, ${tools.reduce((s, t) => s + t.actions.length, 0)} actions\n`);
|
|
59
86
|
process.exit(0);
|
|
@@ -63,33 +90,35 @@ async function main() {
|
|
|
63
90
|
const sandbox = new Sandbox();
|
|
64
91
|
const session = new SessionState();
|
|
65
92
|
const stats = new Stats();
|
|
93
|
+
const thinking = new Thinking();
|
|
94
|
+
const fileTracker = new FileTracker();
|
|
66
95
|
|
|
67
96
|
// 6. Build context & system prompt
|
|
68
97
|
const context = await buildContext();
|
|
69
98
|
const systemPrompt = buildSystemPrompt(registry, config);
|
|
70
|
-
log('info', `Context: ${context.split('\n').length} lines`);
|
|
71
99
|
|
|
72
100
|
// 7. Connect to XiaoZhi
|
|
73
101
|
const mqtt = new MqttClient(config);
|
|
74
102
|
|
|
75
103
|
bus.on('activation:required', ({ code }) => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
console.log(` ╚═════════════════════════════════════════════╝`);
|
|
81
|
-
console.log('');
|
|
104
|
+
startupLogs.push('');
|
|
105
|
+
startupLogs.push(` Kode Aktivasi: ${code}`);
|
|
106
|
+
startupLogs.push(` https://xiaozhi.me`);
|
|
107
|
+
startupLogs.push('');
|
|
82
108
|
});
|
|
83
109
|
|
|
84
110
|
try {
|
|
85
|
-
|
|
86
|
-
await mqtt.connect();
|
|
111
|
+
await withRetry(() => mqtt.connect(), { maxRetries: 2, delay: 2000 });
|
|
87
112
|
} catch (e) {
|
|
88
|
-
log
|
|
113
|
+
console.log = origLog;
|
|
114
|
+
console.error(chalk.red(`\n Connection failed: ${e.message}\n`));
|
|
89
115
|
process.exit(1);
|
|
90
116
|
}
|
|
91
117
|
|
|
92
|
-
// 8.
|
|
118
|
+
// 8. Message sender
|
|
119
|
+
const sender = new MessageSender(mqtt);
|
|
120
|
+
|
|
121
|
+
// 9. Register MCP tool handler
|
|
93
122
|
mqtt.registerToolHandler(
|
|
94
123
|
async (name, args) => {
|
|
95
124
|
const destructive = isDestructive(name, args);
|
|
@@ -98,24 +127,31 @@ async function main() {
|
|
|
98
127
|
if (!ok) return { isError: true, content: [{ type: 'text', text: 'Cancelled by user' }] };
|
|
99
128
|
}
|
|
100
129
|
sandbox.validate(name, args);
|
|
130
|
+
|
|
131
|
+
thinking.set('tool_call');
|
|
101
132
|
const start = Date.now();
|
|
102
|
-
log('tool', `🔧 ${name} → ${args.action || '-'}`);
|
|
103
133
|
try {
|
|
104
134
|
const result = await registry.call(name, args);
|
|
105
135
|
stats.record(name, args.action, !result.isError, Date.now() - start);
|
|
106
136
|
session.addToolCall(name, args, result);
|
|
107
|
-
|
|
137
|
+
|
|
138
|
+
if (name === 'file' && (args.action === 'write' || args.action === 'edit') && args.path) {
|
|
139
|
+
autoFormat(args.path).catch(() => {});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
thinking.set('idle');
|
|
108
143
|
bus.emit('tool:called', { name, result, args });
|
|
109
144
|
return result;
|
|
110
145
|
} catch (e) {
|
|
111
146
|
stats.record(name, args.action, false, Date.now() - start, e.message);
|
|
112
|
-
|
|
147
|
+
thinking.set('idle');
|
|
148
|
+
throw new Error(handleError(e));
|
|
113
149
|
}
|
|
114
150
|
},
|
|
115
151
|
() => registry.toMcpTools()
|
|
116
152
|
);
|
|
117
153
|
|
|
118
|
-
//
|
|
154
|
+
// 10. Optional WSS endpoint
|
|
119
155
|
let wss = null;
|
|
120
156
|
if (config.mcp?.endpoint_url) {
|
|
121
157
|
wss = new WssEndpoint(config.mcp.endpoint_url, () => registry.toMcpTools(), async (name, args) => {
|
|
@@ -125,19 +161,22 @@ async function main() {
|
|
|
125
161
|
wss.start();
|
|
126
162
|
}
|
|
127
163
|
|
|
128
|
-
//
|
|
164
|
+
// 11. Track conversation
|
|
129
165
|
bus.on('user:text', (text) => session.addUserMessage(text));
|
|
130
166
|
bus.on('llm:text', (text) => session.addAiMessage(text));
|
|
131
167
|
|
|
132
|
-
//
|
|
168
|
+
// 12. Save config
|
|
133
169
|
await saveConfig(ROOT, config);
|
|
134
|
-
|
|
135
|
-
|
|
170
|
+
|
|
171
|
+
// 13. Start Ink TUI
|
|
172
|
+
const { unmount, waitUntilExit } = startInkTUI({
|
|
173
|
+
registry, mqtt, stats, session, bus, config,
|
|
174
|
+
extras: { fileTracker, thinking, sender, autoFormat, startupLogs },
|
|
175
|
+
});
|
|
136
176
|
|
|
137
177
|
// Graceful shutdown
|
|
138
178
|
const shutdown = async () => {
|
|
139
|
-
|
|
140
|
-
console.log(stats.formatReport());
|
|
179
|
+
unmount();
|
|
141
180
|
if (wss) wss.stop();
|
|
142
181
|
await registry.shutdown();
|
|
143
182
|
mqtt.disconnect();
|
|
@@ -145,6 +184,12 @@ async function main() {
|
|
|
145
184
|
};
|
|
146
185
|
process.on('SIGINT', shutdown);
|
|
147
186
|
process.on('SIGTERM', shutdown);
|
|
187
|
+
|
|
188
|
+
await waitUntilExit();
|
|
189
|
+
await shutdown();
|
|
148
190
|
}
|
|
149
191
|
|
|
150
|
-
main().catch(e => {
|
|
192
|
+
main().catch(e => {
|
|
193
|
+
console.error(chalk.red(`\n Fatal: ${handleError(e)}\n`));
|
|
194
|
+
process.exit(1);
|
|
195
|
+
});
|
package/src/mcp/activation.js
CHANGED
|
@@ -35,7 +35,7 @@ export async function handleActivation(data, otaUrl, deviceId, clientId) {
|
|
|
35
35
|
|
|
36
36
|
log('warn', `🔐 Activation required`);
|
|
37
37
|
log('info', `📋 Code: ${a.code}`);
|
|
38
|
-
log('info',
|
|
38
|
+
log('info', `[WEB] https://xiaozhi.me`);
|
|
39
39
|
bus.emit('activation:required', { code: a.code });
|
|
40
40
|
|
|
41
41
|
const timeout = a.timeout_ms || 120000;
|
|
@@ -43,10 +43,10 @@ export async function handleActivation(data, otaUrl, deviceId, clientId) {
|
|
|
43
43
|
|
|
44
44
|
while (Date.now() - start < timeout) {
|
|
45
45
|
await new Promise(r => setTimeout(r, 5000));
|
|
46
|
-
log('info', '
|
|
46
|
+
log('info', '[...] Checking activation...');
|
|
47
47
|
const poll = await fetchOtaConfig(otaUrl, deviceId, clientId);
|
|
48
48
|
if (!poll.activation?.code && poll.mqtt) {
|
|
49
|
-
log('ok', '
|
|
49
|
+
log('ok', '[OK] Activated!');
|
|
50
50
|
return poll.mqtt;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -70,7 +70,7 @@ export class MessageSender {
|
|
|
70
70
|
async _waitForReady() {
|
|
71
71
|
if (this.mqtt.mcp.toolsReceived) return;
|
|
72
72
|
|
|
73
|
-
log('info', '
|
|
73
|
+
log('info', '[...] Waiting for MCP handshake...');
|
|
74
74
|
return new Promise((resolve, reject) => {
|
|
75
75
|
const timeout = setTimeout(() => reject(new Error('MCP handshake timeout')), 30000);
|
|
76
76
|
const check = setInterval(() => {
|