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.
- 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 +89 -37
- package/src/mcp/activation.js +2 -5
- 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/README.md
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
# Pelulu CLI
|
|
1
|
+
# Pelulu CLI
|
|
2
2
|
|
|
3
|
-
> A CLI coding agent powered by [XiaoZhi](https://xiaozhi.me) โ
|
|
3
|
+
> A CLI coding agent powered by [XiaoZhi](https://xiaozhi.me) โ the tiny Chinese AI model that lives in ESP32 chips. We gave it 15 tools and a terminal. What could go wrong?
|
|
4
|
+
|
|
5
|
+

|
|
4
6
|
|
|
5
7
|
```
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
/\_/\
|
|
9
|
+
( o.o ) P E L U L U - C L I
|
|
10
|
+
> ^ < v.1.0.1 | powered by XiaoZhi
|
|
11
|
+
/| |\
|
|
12
|
+
(_| |_) the tiny coding companion
|
|
13
|
+
|
|
14
|
+
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
|
|
15
|
+
โ pelulu-cli v.1.0.1 | ONLINE | 594c02b2 | Provider: Xiaozhi.me โ
|
|
16
|
+
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
|
|
13
17
|
```
|
|
14
18
|
|
|
15
19
|
## What is this?
|
|
@@ -21,7 +25,8 @@ Pelulu CLI is a terminal-based AI coding agent that connects to XiaoZhi (a small
|
|
|
21
25
|
## Features
|
|
22
26
|
|
|
23
27
|
- **15 Consolidated Tools** โ 67 actions packed into 15 MCP slots (because XiaoZhi has a 32-tool limit and we're not wasteful)
|
|
24
|
-
- **Rich TUI** โ Box drawing, colors,
|
|
28
|
+
- **Rich TUI** โ Box drawing, colors, scrollable chat. Your terminal will look like a hacker movie from 2003
|
|
29
|
+
- **Scrollable Chat** โ Shift+Up/Down or PageUp/PageDown to scroll through message history
|
|
25
30
|
- **Auto-detect Projects** โ `cd my-project && pelulu` and it figures out the rest
|
|
26
31
|
- **Safety Sandbox** โ Won't let the AI `rm -rf /` (we tested... extensively)
|
|
27
32
|
- **Destructive Op Confirmation** โ Asks before deleting your life's work
|
|
@@ -29,7 +34,8 @@ Pelulu CLI is a terminal-based AI coding agent that connects to XiaoZhi (a small
|
|
|
29
34
|
- **Session Persistence** โ Remembers your device ID so you don't have to re-activate every time
|
|
30
35
|
- **Plugin System** โ Add your own tools (if 67 actions aren't enough for you)
|
|
31
36
|
- **Auto-reconnect** โ MQTT disconnects happen. We handle them. Mostly.
|
|
32
|
-
|
|
37
|
+
- **Auto-format** โ Formats code after edits (prettier/black if available, basic cleanup otherwise)
|
|
38
|
+
- **Mandatory Updates** โ Blocks CLI when a new version is available (keeps everyone on the latest)
|
|
33
39
|
|
|
34
40
|
## Quick Start
|
|
35
41
|
|
|
@@ -58,7 +64,7 @@ First run: you'll get an activation code. Go to [xiaozhi.me](https://xiaozhi.me)
|
|
|
58
64
|
|
|
59
65
|
| Tool | Actions | What it does |
|
|
60
66
|
|------|---------|-------------|
|
|
61
|
-
| `file` | read, write, edit, list, delete, mkdir, copy, move, exists | File operations (9 actions, 1 MCP slot
|
|
67
|
+
| `file` | read, write, edit, list, delete, mkdir, copy, move, exists | File operations (9 actions, 1 MCP slot) |
|
|
62
68
|
| `shell` | exec, bg, ps, kill | Run commands (with safety rails) |
|
|
63
69
|
| `git` | init, clone, status, diff, log, add, commit, push, pull, branch | Full git workflow |
|
|
64
70
|
| `search` | grep, find, web | Search files and the internet |
|
|
@@ -102,86 +108,72 @@ pelulu --wizard # Re-run setup wizard
|
|
|
102
108
|
### Shortcuts (Natural Language)
|
|
103
109
|
|
|
104
110
|
```
|
|
105
|
-
read index.js
|
|
106
|
-
run npm test
|
|
107
|
-
git status
|
|
108
|
-
build
|
|
109
|
-
search TODO
|
|
111
|
+
read index.js -> file read
|
|
112
|
+
run npm test -> shell exec
|
|
113
|
+
git status -> git status
|
|
114
|
+
build -> project build
|
|
115
|
+
search TODO -> search grep
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Scrolling
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
Shift+Up / Shift+Down # Scroll by 5 messages
|
|
122
|
+
PageUp / PageDown # Scroll by 20 messages
|
|
110
123
|
```
|
|
111
124
|
|
|
112
|
-
## Fun Facts
|
|
125
|
+
## Fun Facts
|
|
113
126
|
|
|
114
127
|
- **XiaoZhi's main job** is being a voice assistant for ESP32 microcontrollers. We basically gave a thermostat the ability to `git push --force`
|
|
115
128
|
- **The 32-tool limit** exists because XiaoZhi was designed to control smart home devices, not write your React app. We consolidated 67 actions into 15 tools just to fit
|
|
116
|
-
- **It responds in Mandarin** sometimes. We consider this a feature, not a bug.
|
|
129
|
+
- **It responds in Mandarin** sometimes. We consider this a feature, not a bug.
|
|
117
130
|
- **The MQTT protocol** was chosen because that's what IoT devices use. Your coding agent runs on the same protocol as your smart fridge
|
|
118
131
|
- **XiaoZhi costs approximately $0** to run. Your coding agent is cheaper than a cup of coffee. The quality is... also about that
|
|
119
132
|
- **We tested `rm -rf /`** protection extensively. The AI suggested it exactly once. It was blocked. The AI seemed disappointed
|
|
120
|
-
- **The model runs on a Tenclass server** somewhere in China. Your code is being reviewed by an AI that was trained to turn on lights and play music
|
|
121
|
-
- **15 MCP slots used** out of 32. That's 17 slots free for when someone inevitably asks us to add a "make coffee" tool
|
|
122
133
|
|
|
123
134
|
## Architecture
|
|
124
135
|
|
|
125
136
|
```
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
+--------------+ MQTT +--------------+ MCP +--------------+
|
|
138
|
+
| XiaoZhi AI | <----------> | Pelulu CLI | <----------> | 15 Tools |
|
|
139
|
+
| (tiny LLM) | | (router) | | (67 actions) |
|
|
140
|
+
+--------------+ +--------------+ +--------------+
|
|
141
|
+
|
|
|
142
|
+
+--------------+
|
|
143
|
+
| Ink TUI |
|
|
144
|
+
+--------------+
|
|
134
145
|
```
|
|
135
146
|
|
|
136
147
|
```
|
|
137
148
|
src/
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
โ โโโ completion.js โ Command completion
|
|
167
|
-
โ โโโ retry.js โ Retry logic
|
|
168
|
-
โโโ mcp/
|
|
169
|
-
โ โโโ mqtt-client.js โ MQTT connection
|
|
170
|
-
โ โโโ mcp-handler.js โ MCP protocol
|
|
171
|
-
โ โโโ activation.js โ Device activation
|
|
172
|
-
โ โโโ message-sender.js โ Message queue
|
|
173
|
-
โ โโโ wss-endpoint.js โ WSS endpoint
|
|
174
|
-
โโโ tools/
|
|
175
|
-
โ โโโ file.js, shell.js, git.js, search.js
|
|
176
|
-
โ โโโ project.js, process.js, network.js
|
|
177
|
-
โ โโโ env.js, ai.js, snippet.js
|
|
178
|
-
โ โโโ template.js, history.js, config.js
|
|
179
|
-
โ โโโ diff.js, watch.js
|
|
180
|
-
โโโ plugins/
|
|
181
|
-
โ โโโ manager.js โ Plugin loader
|
|
182
|
-
โโโ tui/
|
|
183
|
-
โโโ renderer.js โ Rich TUI (chalk)
|
|
184
|
-
โโโ status-bar.js โ Status bar
|
|
149
|
+
+-- index.js <- Entry point
|
|
150
|
+
+-- core/
|
|
151
|
+
| +-- config.js <- Config loader
|
|
152
|
+
| +-- event-bus.js <- Pub/sub
|
|
153
|
+
| +-- logger.js <- Logging (Ink-aware)
|
|
154
|
+
| +-- tool-registry.js <- Tool registry
|
|
155
|
+
| +-- sandbox.js <- Safety layer
|
|
156
|
+
| +-- session.js <- Session state
|
|
157
|
+
| +-- system-prompt.js <- Prompt builder
|
|
158
|
+
| +-- auto-format.js <- Code auto-format
|
|
159
|
+
| +-- intent.js <- Natural language parser
|
|
160
|
+
| +-- formatter.js <- Rich output formatting
|
|
161
|
+
| +-- thinking.js <- AI state indicator
|
|
162
|
+
| +-- completion.js <- Command completion
|
|
163
|
+
+-- mcp/
|
|
164
|
+
| +-- mqtt-client.js <- MQTT connection
|
|
165
|
+
| +-- mcp-handler.js <- MCP protocol
|
|
166
|
+
| +-- activation.js <- Device activation
|
|
167
|
+
+-- tools/
|
|
168
|
+
| +-- file.js, shell.js, git.js, search.js, ...
|
|
169
|
+
+-- plugins/
|
|
170
|
+
| +-- manager.js <- Plugin loader
|
|
171
|
+
+-- tui/
|
|
172
|
+
+-- ink-app.js <- Main Ink React app
|
|
173
|
+
+-- ink-components.js <- UI components
|
|
174
|
+
+-- ink-entry.js <- Ink entry point
|
|
175
|
+
+-- renderer.js <- Banner & helpers
|
|
176
|
+
+-- completable-input.js <- Tab completion input
|
|
185
177
|
```
|
|
186
178
|
|
|
187
179
|
## Extending
|
|
@@ -246,11 +238,6 @@ export default {
|
|
|
246
238
|
- Complex requests (8+ files) may timeout. The model needs to think. It's doing its best.
|
|
247
239
|
- The AI occasionally suggests `rm -rf /` as a solution. We block it. The AI learns nothing.
|
|
248
240
|
- MQTT connections can be unstable. We reconnect. It's fine. Everything is fine.
|
|
249
|
-
- The model is small. Like, really small. It runs on chips that cost less than a candy bar.
|
|
250
|
-
|
|
251
|
-
## Why "Nexa"?
|
|
252
|
-
|
|
253
|
-
Because it sounds cool and vaguely futuristic. Also "xiaozhi-cli" was already taken (by us, in a previous life). We wanted something that says "this is a serious developer tool" while secretly knowing it's powered by an AI that was trained to control smart light bulbs.
|
|
254
241
|
|
|
255
242
|
## Contributing
|
|
256
243
|
|
|
@@ -267,7 +254,7 @@ MIT โ do whatever you want with it. If you make money using an AI that was des
|
|
|
267
254
|
---
|
|
268
255
|
|
|
269
256
|
<p align="center">
|
|
270
|
-
Built with
|
|
257
|
+
Built with questionable decisions<br>
|
|
271
258
|
Powered by <a href="https://xiaozhi.me">XiaoZhi</a> โ the little AI that could<br>
|
|
272
259
|
<sub>It's not stupid, it's compact</sub>
|
|
273
260
|
</p>
|
package/config.example.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pelulu-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "CLI coding agent powered by XiaoZhi AI โ a tiny Chinese LLM that lives in ESP32 chips",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node src/index.js",
|
|
12
|
-
"start:text": "node src/index.js --text",
|
|
13
12
|
"start:debug": "node src/index.js --debug",
|
|
13
|
+
"start:fallback": "node src/index.js --no-ink",
|
|
14
14
|
"tools": "node src/index.js --list-tools",
|
|
15
15
|
"test": "node src/index.js --test"
|
|
16
16
|
},
|
|
@@ -35,8 +35,11 @@
|
|
|
35
35
|
"author": "chaerulchas",
|
|
36
36
|
"license": "MIT",
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"chalk": "^5.3.0",
|
|
39
|
+
"ink": "^5.2.1",
|
|
40
|
+
"ink-text-input": "^6.0.0",
|
|
38
41
|
"mqtt": "^5.3.0",
|
|
39
|
-
"
|
|
42
|
+
"react": "^18.3.1"
|
|
40
43
|
},
|
|
41
44
|
"engines": {
|
|
42
45
|
"node": ">=18"
|
package/preview.jpg
ADDED
|
Binary file
|
package/src/core/auto-format.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AutoFormat โ format code after edits
|
|
3
3
|
* Detects project type and runs appropriate formatter
|
|
4
|
+
* Falls back to basic cleanup if no formatter is available
|
|
4
5
|
*/
|
|
5
6
|
import { exec } from 'child_process';
|
|
7
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
6
8
|
import { existsSync } from 'fs';
|
|
7
9
|
import { join } from 'path';
|
|
8
10
|
import { getConfig } from './config.js';
|
|
@@ -23,25 +25,72 @@ async function detectType(cwd) {
|
|
|
23
25
|
return null;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Basic formatter โ no external deps required
|
|
30
|
+
* Trims trailing whitespace, ensures final newline, normalizes blank lines
|
|
31
|
+
*/
|
|
32
|
+
async function basicFormat(filePath) {
|
|
33
|
+
try {
|
|
34
|
+
const content = await readFile(filePath, 'utf-8');
|
|
35
|
+
const formatted = content
|
|
36
|
+
.replace(/[ \t]+$/gm, '') // trailing whitespace
|
|
37
|
+
.replace(/\n{3,}/g, '\n\n') // max 2 consecutive blank lines
|
|
38
|
+
.replace(/\r\n/g, '\n') // normalize line endings
|
|
39
|
+
.replace(/\r/g, '\n');
|
|
40
|
+
|
|
41
|
+
// Ensure final newline
|
|
42
|
+
const final = formatted.endsWith('\n') ? formatted : formatted + '\n';
|
|
43
|
+
|
|
44
|
+
if (final !== content) {
|
|
45
|
+
await writeFile(filePath, final, 'utf-8');
|
|
46
|
+
log('file', `formatted: ${filePath}`);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
debug(`Basic format failed: ${e.message}`);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Try external formatter (prettier, black, etc.)
|
|
58
|
+
* Returns true if successful, false if not available or failed
|
|
59
|
+
*/
|
|
60
|
+
async function externalFormat(formatter, filePath, cwd) {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
exec(`${formatter.cmd} "${filePath}"`, { cwd, timeout: 10000 }, (err, stdout, stderr) => {
|
|
63
|
+
if (err) {
|
|
64
|
+
debug(`External formatter failed: ${err.message}`);
|
|
65
|
+
resolve(false);
|
|
66
|
+
} else {
|
|
67
|
+
log('file', `formatted: ${filePath}`);
|
|
68
|
+
resolve(true);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
26
74
|
export async function autoFormat(filePath) {
|
|
27
75
|
const cfg = getConfig();
|
|
28
76
|
if (!cfg.tools?.auto_format) return; // disabled by default
|
|
29
77
|
|
|
30
78
|
const cwd = cfg.agent?.workspace || process.cwd();
|
|
31
79
|
const type = await detectType(cwd);
|
|
32
|
-
if (!type) return;
|
|
33
|
-
|
|
34
|
-
const formatter = FORMATTERS[type];
|
|
35
|
-
if (!formatter) return;
|
|
36
80
|
|
|
37
81
|
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
38
|
-
|
|
82
|
+
const codeExts = ['js', 'jsx', 'ts', 'tsx', 'json', 'md', 'py', 'rs', 'go', 'css', 'html', 'yaml', 'yml', 'toml'];
|
|
83
|
+
if (!codeExts.includes(ext)) return;
|
|
39
84
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
85
|
+
// Try external formatter first
|
|
86
|
+
if (type && FORMATTERS[type]) {
|
|
87
|
+
const formatter = FORMATTERS[type];
|
|
88
|
+
if (formatter.ext.includes(ext)) {
|
|
89
|
+
const ok = await externalFormat(formatter, filePath, cwd);
|
|
90
|
+
if (ok) return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Fallback to basic format
|
|
95
|
+
await basicFormat(filePath);
|
|
47
96
|
}
|
package/src/core/config.js
CHANGED
|
@@ -33,7 +33,7 @@ export async function loadConfig(root) {
|
|
|
33
33
|
agent: { name: 'Pelulu CLI', version: '1.0.0', workspace: '~/Pelulu-CLI' },
|
|
34
34
|
mqtt: { ota_url: 'https://api.tenclass.net/xiaozhi/ota/', keepalive: 240 },
|
|
35
35
|
mcp: { endpoint_url: '' },
|
|
36
|
-
tools: { shell_timeout: 30000, max_output: 10000 },
|
|
36
|
+
tools: { shell_timeout: 30000, max_output: 10000, auto_format: true },
|
|
37
37
|
plugins: { enabled: [], disabled: [] },
|
|
38
38
|
};
|
|
39
39
|
|
package/src/core/confirm.js
CHANGED
|
@@ -28,7 +28,7 @@ export async function askConfirmation(toolName, args, check) {
|
|
|
28
28
|
if (!check.destructive) return true;
|
|
29
29
|
|
|
30
30
|
const color = check.level === 'danger' ? COLORS.red : COLORS.yellow;
|
|
31
|
-
const icon = check.level === 'danger' ? '
|
|
31
|
+
const icon = check.level === 'danger' ? '[!]' : '[WARN]';
|
|
32
32
|
|
|
33
33
|
console.log(`\n${color}${icon} ${check.msg}${COLORS.reset}`);
|
|
34
34
|
console.log(`${COLORS.dim} Tool: ${toolName}${COLORS.reset}`);
|
|
@@ -44,7 +44,7 @@ export async function askConfirmation(toolName, args, check) {
|
|
|
44
44
|
rl.question(`${color} Continue? (y/N): ${COLORS.reset}`, (answer) => {
|
|
45
45
|
rl.close();
|
|
46
46
|
const ok = answer.trim().toLowerCase() === 'y';
|
|
47
|
-
if (!ok) console.log(`${COLORS.red}
|
|
47
|
+
if (!ok) console.log(`${COLORS.red} [ERR] Cancelled${COLORS.reset}\n`);
|
|
48
48
|
resolve(ok);
|
|
49
49
|
});
|
|
50
50
|
});
|
package/src/core/diff.js
CHANGED
|
@@ -9,7 +9,7 @@ export function showDiff(oldText, newText, filePath) {
|
|
|
9
9
|
const newLines = newText.split('\n');
|
|
10
10
|
const maxLen = Math.max(oldLines.length, newLines.length);
|
|
11
11
|
|
|
12
|
-
console.log(`\n${COLORS.bold}
|
|
12
|
+
console.log(`\n${COLORS.bold}[EDIT] Changes: ${filePath || ''}${COLORS.reset}\n`);
|
|
13
13
|
|
|
14
14
|
let changes = 0;
|
|
15
15
|
for (let i = 0; i < maxLen; i++) {
|
|
@@ -30,7 +30,7 @@ export function showDiff(oldText, newText, filePath) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function showPatch(filePath, hunks) {
|
|
33
|
-
console.log(`\n${COLORS.bold}
|
|
33
|
+
console.log(`\n${COLORS.bold}[EDIT] Patch: ${filePath}${COLORS.reset}\n`);
|
|
34
34
|
for (const hunk of hunks) {
|
|
35
35
|
console.log(`${COLORS.cyan}@@ ${hunk.header || ''} @@${COLORS.reset}`);
|
|
36
36
|
for (const line of hunk.lines) {
|
|
@@ -43,8 +43,8 @@ export function showPatch(filePath, hunks) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function formatFileChange(action, path, details) {
|
|
46
|
-
const icons = { created: '
|
|
47
|
-
const icon = icons[action] || '
|
|
46
|
+
const icons = { created: '[NEW]', modified: '[EDIT]', deleted: '[DEL]', renamed: '[REN]' };
|
|
47
|
+
const icon = icons[action] || '[FILE]';
|
|
48
48
|
const detailStr = details ? ` ${COLORS.dim}(${details})${COLORS.reset}` : '';
|
|
49
49
|
return `${icon} ${action}: ${path}${detailStr}`;
|
|
50
50
|
}
|
package/src/core/doctor.js
CHANGED
|
@@ -48,10 +48,10 @@ export async function runDoctor() {
|
|
|
48
48
|
checks.push({ name: 'Git repo', ok: hasGit, value: hasGit ? 'yes' : 'no' });
|
|
49
49
|
|
|
50
50
|
// Print report
|
|
51
|
-
console.log(`\n${COLORS.bold}
|
|
51
|
+
console.log(`\n${COLORS.bold}[DOC] Doctor Report:${COLORS.reset}\n`);
|
|
52
52
|
let allOk = true;
|
|
53
53
|
for (const c of checks) {
|
|
54
|
-
const icon = c.ok ? `${COLORS.green}
|
|
54
|
+
const icon = c.ok ? `${COLORS.green}[OK]${COLORS.reset}` : `${COLORS.red}[ERR]${COLORS.reset}`;
|
|
55
55
|
console.log(` ${icon} ${c.name.padEnd(18)} ${c.value}`);
|
|
56
56
|
if (!c.ok) allOk = false;
|
|
57
57
|
}
|
|
@@ -21,7 +21,7 @@ export function handleError(error) {
|
|
|
21
21
|
const code = error.code || error.constructor?.name || 'UNKNOWN';
|
|
22
22
|
const suggestion = ERROR_SUGGESTIONS[code]?.(error) || null;
|
|
23
23
|
|
|
24
|
-
const lines = [`${COLORS.red}
|
|
24
|
+
const lines = [`${COLORS.red}[ERR] ${error.message}${COLORS.reset}`];
|
|
25
25
|
if (suggestion) lines.push(`${COLORS.yellow}๐ก ${suggestion}${COLORS.reset}`);
|
|
26
26
|
|
|
27
27
|
return lines.join('\n');
|
package/src/core/file-tracker.js
CHANGED
|
@@ -32,10 +32,10 @@ export class FileTracker {
|
|
|
32
32
|
const modified = changes.filter(c => c.action === 'modified');
|
|
33
33
|
const deleted = changes.filter(c => c.action === 'deleted');
|
|
34
34
|
|
|
35
|
-
const lines = [`${COLORS.bold}
|
|
36
|
-
if (created.length) lines.push(` ${COLORS.green}
|
|
37
|
-
if (modified.length) lines.push(` ${COLORS.yellow}
|
|
38
|
-
if (deleted.length) lines.push(` ${COLORS.red}
|
|
35
|
+
const lines = [`${COLORS.bold}[DIR] File Changes:${COLORS.reset}`];
|
|
36
|
+
if (created.length) lines.push(` ${COLORS.green}[NEW] Created:${COLORS.reset} ${created.map(c => c.path).join(', ')}`);
|
|
37
|
+
if (modified.length) lines.push(` ${COLORS.yellow}[EDIT] Modified:${COLORS.reset} ${modified.map(c => `${c.path} (${c.count}x)`).join(', ')}`);
|
|
38
|
+
if (deleted.length) lines.push(` ${COLORS.red}[DEL] Deleted:${COLORS.reset} ${deleted.map(c => c.path).join(', ')}`);
|
|
39
39
|
|
|
40
40
|
return lines.join('\n');
|
|
41
41
|
}
|
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 {
|