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.
Files changed (43) hide show
  1. package/README.md +69 -82
  2. package/config.example.json +2 -1
  3. package/package.json +6 -3
  4. package/preview.jpg +0 -0
  5. package/src/core/auto-format.js +61 -12
  6. package/src/core/config.js +1 -1
  7. package/src/core/confirm.js +2 -2
  8. package/src/core/diff.js +4 -4
  9. package/src/core/doctor.js +2 -2
  10. package/src/core/error-handler.js +1 -1
  11. package/src/core/file-tracker.js +4 -4
  12. package/src/core/formatter.js +19 -19
  13. package/src/core/logger.js +19 -3
  14. package/src/core/spinner.js +2 -2
  15. package/src/core/stats.js +2 -2
  16. package/src/core/thinking.js +8 -8
  17. package/src/core/tool-help.js +2 -2
  18. package/src/core/tool-registry.js +6 -10
  19. package/src/core/update-checker.js +125 -0
  20. package/src/core/wizard.js +5 -5
  21. package/src/core/workspace.js +4 -4
  22. package/src/index.js +89 -37
  23. package/src/mcp/activation.js +2 -5
  24. package/src/mcp/message-sender.js +1 -1
  25. package/src/mcp/mqtt-client.js +4 -4
  26. package/src/repl-commands.js +2 -1
  27. package/src/repl.js +26 -6
  28. package/src/tools/config.js +1 -1
  29. package/src/tools/diff.js +6 -0
  30. package/src/tools/file.js +3 -3
  31. package/src/tools/git.js +1 -1
  32. package/src/tools/network.js +1 -1
  33. package/src/tools/project.js +2 -2
  34. package/src/tools/search.js +3 -3
  35. package/src/tools/shell.js +2 -2
  36. package/src/tools/template.js +1 -1
  37. package/src/tools/watch.js +1 -1
  38. package/src/tui/completable-input.js +127 -0
  39. package/src/tui/ink-app.js +324 -0
  40. package/src/tui/ink-components.js +157 -0
  41. package/src/tui/ink-entry.js +86 -0
  42. package/src/tui/renderer.js +137 -19
  43. 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) โ€” yes, the tiny Chinese AI model that lives in ESP32 chips. We gave it 15 tools and a terminal. What could go wrong?
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
+ ![Pelulu CLI Preview](preview.jpg)
4
6
 
5
7
  ```
6
- โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
7
- โ”‚ ๐Ÿพ Pelulu CLI โ”‚
8
- โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
9
- โ”‚ ๐Ÿ“ my-project โ”‚
10
- โ”‚ ๐Ÿ”ง 15 tools ยท 67 actions ยท 15 MCP slots used โ”‚
11
- โ”‚ ๐ŸŸข Connected to XiaoZhi โ”‚
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, emoji. Your terminal will look like a hacker movie from 2003
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 โ€” efficiencyโ„ข) |
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 โ†’ file read
106
- run npm test โ†’ shell exec
107
- git status โ†’ git status
108
- build โ†’ project build
109
- search TODO โ†’ search grep
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
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” MQTT โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” MCP โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
127
- โ”‚ XiaoZhi AI โ”‚ โ†โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’ โ”‚ Pelulu CLI โ”‚ โ†โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’ โ”‚ 15 Tools โ”‚
128
- โ”‚ (tiny LLM) โ”‚ โ”‚ (router) โ”‚ โ”‚ (67 actions)โ”‚
129
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
130
- โ†•
131
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
132
- โ”‚ Rich TUI โ”‚
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
- โ”œโ”€โ”€ index.js โ† Entry point
139
- โ”œโ”€โ”€ repl.js โ† CLI interface
140
- โ”œโ”€โ”€ repl-commands.js โ† Command handlers
141
- โ”œโ”€โ”€ core/
142
- โ”‚ โ”œโ”€โ”€ config.js โ† Config loader
143
- โ”‚ โ”œโ”€โ”€ event-bus.js โ† Pub/sub
144
- โ”‚ โ”œโ”€โ”€ logger.js โ† Logging
145
- โ”‚ โ”œโ”€โ”€ tool-registry.js โ† Tool registry
146
- โ”‚ โ”œโ”€โ”€ sandbox.js โ† Safety layer
147
- โ”‚ โ”œโ”€โ”€ session.js โ† Session state
148
- โ”‚ โ”œโ”€โ”€ system-prompt.js โ† Prompt builder
149
- โ”‚ โ”œโ”€โ”€ context.js โ† Context injection
150
- โ”‚ โ”œโ”€โ”€ confirm.js โ† Confirmation system
151
- โ”‚ โ”œโ”€โ”€ diff.js โ† Diff display
152
- โ”‚ โ”œโ”€โ”€ spinner.js โ† Progress indicators
153
- โ”‚ โ”œโ”€โ”€ workspace.js โ† Workspace detection
154
- โ”‚ โ”œโ”€โ”€ stats.js โ† Usage analytics
155
- โ”‚ โ”œโ”€โ”€ conversation.js โ† Conv export/import
156
- โ”‚ โ”œโ”€โ”€ wizard.js โ† Config wizard
157
- โ”‚ โ”œโ”€โ”€ formatter.js โ† Rich output formatting
158
- โ”‚ โ”œโ”€โ”€ tool-help.js โ† Tool examples
159
- โ”‚ โ”œโ”€โ”€ intent.js โ† Natural language parser
160
- โ”‚ โ”œโ”€โ”€ file-tracker.js โ† File change tracking
161
- โ”‚ โ”œโ”€โ”€ auto-format.js โ† Code auto-format
162
- โ”‚ โ”œโ”€โ”€ error-handler.js โ† Smart errors
163
- โ”‚ โ”œโ”€โ”€ thinking.js โ† AI state indicator
164
- โ”‚ โ”œโ”€โ”€ model-info.js โ† Model info
165
- โ”‚ โ”œโ”€โ”€ keybindings.js โ† Keyboard shortcuts
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 โค๏ธ and questionable decisions<br>
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>
@@ -15,7 +15,8 @@
15
15
  },
16
16
  "tools": {
17
17
  "shell_timeout": 30000,
18
- "max_output": 10000
18
+ "max_output": 10000,
19
+ "auto_format": true
19
20
  },
20
21
  "plugins": {
21
22
  "enabled": [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pelulu-cli",
3
- "version": "1.0.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
- "chalk": "^5.3.0"
42
+ "react": "^18.3.1"
40
43
  },
41
44
  "engines": {
42
45
  "node": ">=18"
package/preview.jpg ADDED
Binary file
@@ -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
- if (!formatter.ext.includes(ext)) return;
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
- return new Promise((resolve) => {
41
- exec(`${formatter.cmd} "${filePath}"`, { cwd, timeout: 10000 }, (err, stdout, stderr) => {
42
- if (err) debug(`Auto-format failed: ${err.message}`);
43
- else log('file', `๐ŸŽจ Formatted: ${filePath}`);
44
- resolve(!err);
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
  }
@@ -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
 
@@ -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} โŒ Cancelled${COLORS.reset}\n`);
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}๐Ÿ“ Changes: ${filePath || ''}${COLORS.reset}\n`);
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}๐Ÿ“ Patch: ${filePath}${COLORS.reset}\n`);
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: 'โœจ', modified: '๐Ÿ“', deleted: '๐Ÿ—‘๏ธ', renamed: '๐Ÿ“Ž' };
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
  }
@@ -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}๐Ÿฉบ Doctor Report:${COLORS.reset}\n`);
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}โœ…${COLORS.reset}` : `${COLORS.red}โŒ${COLORS.reset}`;
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}โŒ ${error.message}${COLORS.reset}`];
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');
@@ -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}๐Ÿ“ File Changes:${COLORS.reset}`];
36
- if (created.length) lines.push(` ${COLORS.green}โœจ Created:${COLORS.reset} ${created.map(c => c.path).join(', ')}`);
37
- if (modified.length) lines.push(` ${COLORS.yellow}๐Ÿ“ Modified:${COLORS.reset} ${modified.map(c => `${c.path} (${c.count}x)`).join(', ')}`);
38
- if (deleted.length) lines.push(` ${COLORS.red}๐Ÿ—‘๏ธ Deleted:${COLORS.reset} ${deleted.map(c => c.path).join(', ')}`);
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
  }
@@ -25,35 +25,35 @@ function tryParse(text) {
25
25
  }
26
26
 
27
27
  function formatError(text) {
28
- return `${COLORS.red}โŒ ${text}${COLORS.reset}`;
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}โœ…${COLORS.reset} ${data.path} (${lines} lines, ${data.totalSize} chars)`;
34
+ return `${COLORS.green}[OK]${COLORS.reset} ${data.path} (${lines} lines, ${data.totalSize} chars)`;
35
35
  }
36
- if (action === 'write') return `${COLORS.green}โœ…${COLORS.reset} Written: ${data.path} (${data.written} chars)`;
37
- if (action === 'edit') return `${COLORS.green}โœ…${COLORS.reset} Edited: ${data.path} (${data.occurrences} occurrence(s))`;
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}โœ…${COLORS.reset} ${data.path}: ${dirs} dirs, ${files} files`;
41
+ return `${COLORS.green}[OK]${COLORS.reset} ${data.path}: ${dirs} dirs, ${files} files`;
42
42
  }
43
- if (action === 'exists') return data.exists ? `โœ… ${data.type}: ${data.path}` : `โŒ Not found: ${data.path}`;
44
- if (action === 'delete') return `${COLORS.yellow}๐Ÿ—‘๏ธ${COLORS.reset} Deleted: ${data.path}`;
45
- return `${COLORS.green}โœ…${COLORS.reset} ${action}: ${JSON.stringify(data)}`;
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 `๐ŸŒฟ ${data.branch} (${dirty}) ${data.changes?.length || 0} changes`;
51
+ return `${data.branch} (${dirty}) ${data.changes?.length || 0} changes`;
52
52
  }
53
- if (action === 'log') return `๐Ÿ“œ ${data.commits?.length || 0} commits:\n ${data.commits?.join('\n ')}`;
54
- if (action === 'commit') return data.committed ? `${COLORS.green}โœ…${COLORS.reset} Committed: ${data.message}` : `โš ๏ธ ${data.reason}`;
55
- if (action === 'branch') return data.branches ? `๐ŸŒฟ Branches:\n ${data.branches.join('\n ')}` : `โœ… ${data.created || data.deleted}`;
56
- return `${COLORS.green}โœ…${COLORS.reset} git ${action}: ${JSON.stringify(data)}`;
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 `๐Ÿ” ${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)}`;
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}โœ… Build OK${COLORS.reset} (${data.type})` : `${COLORS.red}โŒ Build failed${COLORS.reset} (exit ${data.exitCode})`;
76
- if (action === 'test') return data.passed ? `${COLORS.green}โœ… Tests passed${COLORS.reset}` : `${COLORS.red}โŒ Tests failed${COLORS.reset} (exit ${data.exitCode})`;
77
- if (action === 'info') return `๐Ÿ“ ${data.name || data.type}: ${data.scripts?.join(', ') || 'no scripts'}`;
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
 
@@ -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: 'โœ…', err: 'โŒ', warn: 'โš ๏ธ', info: 'โ„น๏ธ',
21
- tool: '๐Ÿ”ง', mcp: '๐Ÿ”—', plugin: '๐Ÿงฉ', system: 'โš™๏ธ',
22
- user: '๐Ÿ‘ค', ai: '๐Ÿค–', debug: '๐Ÿ”', file: '๐Ÿ“',
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,
@@ -39,11 +39,11 @@ export class Spinner {
39
39
  }
40
40
 
41
41
  success(text) {
42
- this.stop(`${COLORS.green}โœ… ${text}${COLORS.reset}`);
42
+ this.stop(`${COLORS.green}[OK] ${text}${COLORS.reset}`);
43
43
  }
44
44
 
45
45
  fail(text) {
46
- this.stop(`${COLORS.red}โŒ ${text}${COLORS.reset}`);
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
- `๐Ÿ“Š Session Stats`,
44
+ `[STATS] Session Stats`,
45
45
  ` Uptime: ${s.uptime}s`,
46
- ` Tool calls: ${s.total} (${s.success} โœ… / ${s.failed} โŒ)`,
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) {
@@ -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: '๐Ÿ’ค', 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...' },
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 {