pelulu-cli 1.0.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/README.md +273 -0
- package/config.example.json +24 -0
- package/package.json +44 -0
- package/src/core/auto-format.js +47 -0
- package/src/core/completion.js +65 -0
- package/src/core/config.js +68 -0
- package/src/core/confirm.js +51 -0
- package/src/core/context.js +69 -0
- package/src/core/conversation.js +57 -0
- package/src/core/diff.js +50 -0
- package/src/core/doctor.js +60 -0
- package/src/core/error-handler.js +39 -0
- package/src/core/event-bus.js +43 -0
- package/src/core/file-tracker.js +44 -0
- package/src/core/formatter.js +83 -0
- package/src/core/intent.js +81 -0
- package/src/core/keybindings.js +29 -0
- package/src/core/logger.js +67 -0
- package/src/core/model-info.js +26 -0
- package/src/core/retry.js +26 -0
- package/src/core/sandbox.js +54 -0
- package/src/core/session.js +46 -0
- package/src/core/spinner.js +60 -0
- package/src/core/stats.js +73 -0
- package/src/core/system-prompt.js +42 -0
- package/src/core/thinking.js +39 -0
- package/src/core/tool-help.js +117 -0
- package/src/core/tool-registry.js +82 -0
- package/src/core/wizard.js +55 -0
- package/src/core/workspace.js +96 -0
- package/src/index.js +150 -0
- package/src/mcp/activation.js +54 -0
- package/src/mcp/mcp-handler.js +92 -0
- package/src/mcp/message-sender.js +147 -0
- package/src/mcp/mqtt-client.js +160 -0
- package/src/mcp/wss-endpoint.js +133 -0
- package/src/plugins/manager.js +72 -0
- package/src/repl-commands.js +91 -0
- package/src/repl.js +115 -0
- package/src/tools/ai.js +128 -0
- package/src/tools/config.js +87 -0
- package/src/tools/diff.js +118 -0
- package/src/tools/env.js +58 -0
- package/src/tools/file.js +177 -0
- package/src/tools/git.js +166 -0
- package/src/tools/history.js +64 -0
- package/src/tools/network.js +87 -0
- package/src/tools/process.js +69 -0
- package/src/tools/project.js +152 -0
- package/src/tools/search.js +100 -0
- package/src/tools/shell.js +91 -0
- package/src/tools/snippet.js +95 -0
- package/src/tools/template.js +113 -0
- package/src/tools/watch.js +87 -0
- package/src/tui/renderer.js +119 -0
- package/src/tui/status-bar.js +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Pelulu CLI ๐พ
|
|
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?
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
|
|
7
|
+
โ ๐พ Pelulu CLI โ
|
|
8
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
|
|
9
|
+
โ ๐ my-project โ
|
|
10
|
+
โ ๐ง 15 tools ยท 67 actions ยท 15 MCP slots used โ
|
|
11
|
+
โ ๐ข Connected to XiaoZhi โ
|
|
12
|
+
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What is this?
|
|
16
|
+
|
|
17
|
+
Pelulu CLI is a terminal-based AI coding agent that connects to XiaoZhi (a small Chinese LLM designed for IoT devices) via MQTT and MCP protocol. It gives the AI access to 15 consolidated tools so it can read your files, run commands, manage git, and generally pretend to be a competent developer.
|
|
18
|
+
|
|
19
|
+
**Why XiaoZhi?** Because sometimes you don't need GPT-4 to write your `hello.txt`. Sometimes you need a model that runs on a $3 chip and responds in Mandarin. We're not here to judge.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- **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
|
|
25
|
+
- **Auto-detect Projects** โ `cd my-project && pelulu` and it figures out the rest
|
|
26
|
+
- **Safety Sandbox** โ Won't let the AI `rm -rf /` (we tested... extensively)
|
|
27
|
+
- **Destructive Op Confirmation** โ Asks before deleting your life's work
|
|
28
|
+
- **Intent Parser** โ Type `read index.js` instead of `/call file read {"path":"index.js"}` like a civilized person
|
|
29
|
+
- **Session Persistence** โ Remembers your device ID so you don't have to re-activate every time
|
|
30
|
+
- **Plugin System** โ Add your own tools (if 67 actions aren't enough for you)
|
|
31
|
+
- **Auto-reconnect** โ MQTT disconnects happen. We handle them. Mostly.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Install globally from npm
|
|
38
|
+
npm install -g pelulu-cli
|
|
39
|
+
|
|
40
|
+
# Go to any project and run
|
|
41
|
+
cd ~/my-awesome-project
|
|
42
|
+
pelulu
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or from source:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/venenapro/Pelulu-CLI.git
|
|
49
|
+
cd Pelulu-CLI
|
|
50
|
+
npm install
|
|
51
|
+
npm link
|
|
52
|
+
pelulu
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
First run: you'll get an activation code. Go to [xiaozhi.me](https://xiaozhi.me), enter it, and boom โ your tiny AI overlord is ready.
|
|
56
|
+
|
|
57
|
+
## Tools
|
|
58
|
+
|
|
59
|
+
| Tool | Actions | What it does |
|
|
60
|
+
|------|---------|-------------|
|
|
61
|
+
| `file` | read, write, edit, list, delete, mkdir, copy, move, exists | File operations (9 actions, 1 MCP slot โ efficiencyโข) |
|
|
62
|
+
| `shell` | exec, bg, ps, kill | Run commands (with safety rails) |
|
|
63
|
+
| `git` | init, clone, status, diff, log, add, commit, push, pull, branch | Full git workflow |
|
|
64
|
+
| `search` | grep, find, web | Search files and the internet |
|
|
65
|
+
| `project` | init, build, test, lint, deps, info | Auto-detect Node/Python/Rust/Go/Java |
|
|
66
|
+
| `process` | list, info, kill, top | Process management |
|
|
67
|
+
| `network` | fetch, download, ping | HTTP requests |
|
|
68
|
+
| `env` | get, set, list | Environment variables |
|
|
69
|
+
| `ai` | explain, analyze, detectLanguage, summarize, diff | Code analysis |
|
|
70
|
+
| `snippet` | save, load, list, delete | Code snippet library |
|
|
71
|
+
| `template` | list, create, info | Project scaffolding |
|
|
72
|
+
| `history` | list, clear, stats | Tool call history |
|
|
73
|
+
| `config` | get, set, list, reset | Runtime configuration |
|
|
74
|
+
| `diff` | compare, stats, patch | File comparison |
|
|
75
|
+
| `watch` | start, stop, status | File change monitoring |
|
|
76
|
+
|
|
77
|
+
## Commands
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
pelulu # Start interactive REPL in current directory
|
|
81
|
+
pelulu --list-tools # Show all available tools
|
|
82
|
+
pelulu --debug # Enable debug logging
|
|
83
|
+
pelulu --wizard # Re-run setup wizard
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### In-App Commands
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
/tools # Show MCP tools
|
|
90
|
+
/help <tool> # Tool examples (e.g. /help file)
|
|
91
|
+
/status # Connection & session info
|
|
92
|
+
/stats # Usage statistics
|
|
93
|
+
/workspace # Project info
|
|
94
|
+
/files # File changes this session
|
|
95
|
+
/call <tool> # Call tool directly
|
|
96
|
+
/doctor # Health check
|
|
97
|
+
/model # Model info
|
|
98
|
+
/clear # Clear screen
|
|
99
|
+
/quit # Exit
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Shortcuts (Natural Language)
|
|
103
|
+
|
|
104
|
+
```
|
|
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
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Fun Facts ๐
|
|
113
|
+
|
|
114
|
+
- **XiaoZhi's main job** is being a voice assistant for ESP32 microcontrollers. We basically gave a thermostat the ability to `git push --force`
|
|
115
|
+
- **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. ้ๅธธๅฅฝ๏ผ
|
|
117
|
+
- **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
|
+
- **XiaoZhi costs approximately $0** to run. Your coding agent is cheaper than a cup of coffee. The quality is... also about that
|
|
119
|
+
- **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
|
+
|
|
123
|
+
## Architecture
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
โโโโโโโโโโโโโโโโ MQTT โโโโโโโโโโโโโโโโ MCP โโโโโโโโโโโโโโโโ
|
|
127
|
+
โ XiaoZhi AI โ โโโโโโโโโโโโ โ Pelulu CLI โ โโโโโโโโโโโโ โ 15 Tools โ
|
|
128
|
+
โ (tiny LLM) โ โ (router) โ โ (67 actions)โ
|
|
129
|
+
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
|
|
130
|
+
โ
|
|
131
|
+
โโโโโโโโโโโโโโโโ
|
|
132
|
+
โ Rich TUI โ
|
|
133
|
+
โโโโโโโโโโโโโโโโ
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
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
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Extending
|
|
188
|
+
|
|
189
|
+
### Add a New Tool
|
|
190
|
+
|
|
191
|
+
Create `src/tools/mytool.js`:
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
const ACTIONS = {
|
|
195
|
+
greet: {
|
|
196
|
+
required: ['name'],
|
|
197
|
+
handler: async ({ name }) => ({ message: `Hello, ${name}!` }),
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const actionNames = Object.keys(ACTIONS);
|
|
202
|
+
|
|
203
|
+
export default {
|
|
204
|
+
name: 'mytool',
|
|
205
|
+
description: 'My custom tool',
|
|
206
|
+
actions: actionNames.map(name => ({ name, required: ACTIONS[name].required })),
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: {
|
|
210
|
+
action: { type: 'string', enum: actionNames },
|
|
211
|
+
name: { type: 'string' },
|
|
212
|
+
},
|
|
213
|
+
required: ['action'],
|
|
214
|
+
},
|
|
215
|
+
async handler({ action, ...params }) {
|
|
216
|
+
const a = ACTIONS[action];
|
|
217
|
+
if (!a) throw new Error(`Unknown: ${action}`);
|
|
218
|
+
for (const f of a.required) {
|
|
219
|
+
if (params[f] === undefined) throw new Error(`Missing: ${f}`);
|
|
220
|
+
}
|
|
221
|
+
return a.handler(params);
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Auto-loaded on next restart. Uses 1 MCP slot.
|
|
227
|
+
|
|
228
|
+
### Add a Plugin
|
|
229
|
+
|
|
230
|
+
Create `src/plugins/myplugin.js`:
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
export default {
|
|
234
|
+
name: 'myplugin',
|
|
235
|
+
version: '1.0.0',
|
|
236
|
+
description: 'My plugin',
|
|
237
|
+
async init({ bus, config }) { },
|
|
238
|
+
tools: [{ name: '...', description: '...', inputSchema: {...}, handler: async () => {} }],
|
|
239
|
+
async shutdown() { },
|
|
240
|
+
};
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Known Issues
|
|
244
|
+
|
|
245
|
+
- XiaoZhi sometimes responds in Mandarin. This is a feature.
|
|
246
|
+
- Complex requests (8+ files) may timeout. The model needs to think. It's doing its best.
|
|
247
|
+
- The AI occasionally suggests `rm -rf /` as a solution. We block it. The AI learns nothing.
|
|
248
|
+
- 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
|
+
|
|
255
|
+
## Contributing
|
|
256
|
+
|
|
257
|
+
1. Fork it
|
|
258
|
+
2. Create your feature branch (`git checkout -b feature/amazing`)
|
|
259
|
+
3. Commit your changes (`git commit -m 'feat: add amazing'`)
|
|
260
|
+
4. Push to the branch (`git push origin feature/amazing`)
|
|
261
|
+
5. Open a Pull Request
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
MIT โ do whatever you want with it. If you make money using an AI that was designed for ESP32 chips, you deserve it.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
<p align="center">
|
|
270
|
+
Built with โค๏ธ and questionable decisions<br>
|
|
271
|
+
Powered by <a href="https://xiaozhi.me">XiaoZhi</a> โ the little AI that could<br>
|
|
272
|
+
<sub>It's not stupid, it's compact</sub>
|
|
273
|
+
</p>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"agent": {
|
|
3
|
+
"name": "Nexa CLI",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"workspace": "~/Nexa-CLI"
|
|
6
|
+
},
|
|
7
|
+
"mqtt": {
|
|
8
|
+
"ota_url": "https://api.tenclass.net/xiaozhi/ota/",
|
|
9
|
+
"keepalive": 240,
|
|
10
|
+
"reconnect_period": 5000,
|
|
11
|
+
"connect_timeout": 10000
|
|
12
|
+
},
|
|
13
|
+
"mcp": {
|
|
14
|
+
"endpoint_url": ""
|
|
15
|
+
},
|
|
16
|
+
"tools": {
|
|
17
|
+
"shell_timeout": 30000,
|
|
18
|
+
"max_output": 10000
|
|
19
|
+
},
|
|
20
|
+
"plugins": {
|
|
21
|
+
"enabled": [],
|
|
22
|
+
"disabled": []
|
|
23
|
+
}
|
|
24
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pelulu-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI coding agent powered by XiaoZhi AI โ a tiny Chinese LLM that lives in ESP32 chips",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"pelulu": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"start:text": "node src/index.js --text",
|
|
13
|
+
"start:debug": "node src/index.js --debug",
|
|
14
|
+
"tools": "node src/index.js --list-tools",
|
|
15
|
+
"test": "node src/index.js --test"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"cli",
|
|
19
|
+
"coding-agent",
|
|
20
|
+
"ai",
|
|
21
|
+
"xiaozhi",
|
|
22
|
+
"mcp",
|
|
23
|
+
"mqtt",
|
|
24
|
+
"terminal",
|
|
25
|
+
"developer-tools"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/chaerulchas/Pelulu-CLI.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/chaerulchas/Pelulu-CLI/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/chaerulchas/Pelulu-CLI#readme",
|
|
35
|
+
"author": "chaerulchas",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"mqtt": "^5.3.0",
|
|
39
|
+
"chalk": "^5.3.0"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AutoFormat โ format code after edits
|
|
3
|
+
* Detects project type and runs appropriate formatter
|
|
4
|
+
*/
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { getConfig } from './config.js';
|
|
9
|
+
import { log, debug } from './logger.js';
|
|
10
|
+
|
|
11
|
+
const FORMATTERS = {
|
|
12
|
+
node: { cmd: 'npx prettier --write', ext: ['js', 'jsx', 'ts', 'tsx', 'json', 'md'] },
|
|
13
|
+
python: { cmd: 'python -m black', ext: ['py'] },
|
|
14
|
+
rust: { cmd: 'rustfmt', ext: ['rs'] },
|
|
15
|
+
go: { cmd: 'gofmt -w', ext: ['go'] },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
async function detectType(cwd) {
|
|
19
|
+
if (existsSync(join(cwd, 'package.json'))) return 'node';
|
|
20
|
+
if (existsSync(join(cwd, 'pyproject.toml')) || existsSync(join(cwd, 'requirements.txt'))) return 'python';
|
|
21
|
+
if (existsSync(join(cwd, 'Cargo.toml'))) return 'rust';
|
|
22
|
+
if (existsSync(join(cwd, 'go.mod'))) return 'go';
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function autoFormat(filePath) {
|
|
27
|
+
const cfg = getConfig();
|
|
28
|
+
if (!cfg.tools?.auto_format) return; // disabled by default
|
|
29
|
+
|
|
30
|
+
const cwd = cfg.agent?.workspace || process.cwd();
|
|
31
|
+
const type = await detectType(cwd);
|
|
32
|
+
if (!type) return;
|
|
33
|
+
|
|
34
|
+
const formatter = FORMATTERS[type];
|
|
35
|
+
if (!formatter) return;
|
|
36
|
+
|
|
37
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
38
|
+
if (!formatter.ext.includes(ext)) return;
|
|
39
|
+
|
|
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
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Completion โ command completion hints for REPL
|
|
3
|
+
*/
|
|
4
|
+
const COMMANDS = [
|
|
5
|
+
'/tools', '/help', '/status', '/stats', '/workspace', '/files',
|
|
6
|
+
'/call', '/conv', '/history', '/doctor', '/model', '/clear', '/quit',
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
const TOOL_NAMES = [
|
|
10
|
+
'file', 'shell', 'git', 'search', 'project', 'process',
|
|
11
|
+
'network', 'env', 'ai', 'snippet', 'template', 'history',
|
|
12
|
+
'config', 'diff', 'watch',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const ACTIONS = {
|
|
16
|
+
file: ['read', 'write', 'edit', 'list', 'delete', 'mkdir', 'copy', 'move', 'exists'],
|
|
17
|
+
shell: ['exec', 'bg', 'ps', 'kill'],
|
|
18
|
+
git: ['init', 'clone', 'status', 'diff', 'log', 'add', 'commit', 'push', 'pull', 'branch'],
|
|
19
|
+
search: ['grep', 'find', 'web'],
|
|
20
|
+
project: ['init', 'build', 'test', 'lint', 'deps', 'info'],
|
|
21
|
+
process: ['list', 'info', 'kill', 'top'],
|
|
22
|
+
network: ['fetch', 'download', 'ping'],
|
|
23
|
+
env: ['get', 'set', 'list'],
|
|
24
|
+
ai: ['explain', 'analyze', 'detectLanguage', 'summarize', 'diff'],
|
|
25
|
+
snippet: ['save', 'load', 'list', 'delete'],
|
|
26
|
+
template: ['list', 'create', 'info'],
|
|
27
|
+
history: ['list', 'clear', 'stats'],
|
|
28
|
+
config: ['get', 'set', 'list', 'reset'],
|
|
29
|
+
diff: ['compare', 'stats', 'patch'],
|
|
30
|
+
watch: ['start', 'stop', 'status'],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function getCompletions(input) {
|
|
34
|
+
const parts = input.trim().split(/\s+/);
|
|
35
|
+
|
|
36
|
+
// Command completion
|
|
37
|
+
if (parts.length === 1 && parts[0].startsWith('/')) {
|
|
38
|
+
return COMMANDS.filter(c => c.startsWith(parts[0]));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Tool name completion after /call
|
|
42
|
+
if (parts[0] === '/call' && parts.length === 2) {
|
|
43
|
+
return TOOL_NAMES.filter(t => t.startsWith(parts[1]));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Action completion after /call <tool>
|
|
47
|
+
if (parts[0] === '/call' && parts.length === 3) {
|
|
48
|
+
const tool = parts[1];
|
|
49
|
+
const actions = ACTIONS[tool];
|
|
50
|
+
if (actions) return actions.filter(a => a.startsWith(parts[2]));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Intent completion
|
|
54
|
+
if (parts.length === 1) {
|
|
55
|
+
const intents = ['read', 'write', 'edit', 'run', 'git', 'build', 'test', 'search', 'ls', 'mkdir', 'kill', 'fetch', 'ping', 'analyze'];
|
|
56
|
+
return intents.filter(i => i.startsWith(parts[0]));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function formatCompletions(completions) {
|
|
63
|
+
if (!completions.length) return '';
|
|
64
|
+
return completions.map(c => ` ${c}`).join('\n');
|
|
65
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config โ load, merge, save configuration
|
|
3
|
+
*/
|
|
4
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
|
|
9
|
+
const CONFIG_FILE = 'config.json';
|
|
10
|
+
const HOME = homedir();
|
|
11
|
+
|
|
12
|
+
function expand(p) {
|
|
13
|
+
return p?.replace(/^~(?=$|[/\\])/g, HOME) ?? p;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function deepMerge(target, source) {
|
|
17
|
+
const result = { ...target };
|
|
18
|
+
for (const key of Object.keys(source)) {
|
|
19
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
20
|
+
result[key] = deepMerge(result[key] || {}, source[key]);
|
|
21
|
+
} else {
|
|
22
|
+
result[key] = source[key];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let _config = null;
|
|
29
|
+
|
|
30
|
+
export async function loadConfig(root) {
|
|
31
|
+
const path = join(root, CONFIG_FILE);
|
|
32
|
+
const defaults = {
|
|
33
|
+
agent: { name: 'Pelulu CLI', version: '1.0.0', workspace: '~/Pelulu-CLI' },
|
|
34
|
+
mqtt: { ota_url: 'https://api.tenclass.net/xiaozhi/ota/', keepalive: 240 },
|
|
35
|
+
mcp: { endpoint_url: '' },
|
|
36
|
+
tools: { shell_timeout: 30000, max_output: 10000 },
|
|
37
|
+
plugins: { enabled: [], disabled: [] },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (existsSync(path)) {
|
|
41
|
+
try {
|
|
42
|
+
const raw = await readFile(path, 'utf-8');
|
|
43
|
+
_config = deepMerge(defaults, JSON.parse(raw));
|
|
44
|
+
} catch { _config = defaults; }
|
|
45
|
+
} else {
|
|
46
|
+
_config = defaults;
|
|
47
|
+
await saveConfig(root, _config);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_config._root = root;
|
|
51
|
+
_config._path = path;
|
|
52
|
+
_config.agent.workspace = expand(_config.agent.workspace);
|
|
53
|
+
return _config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getConfig() {
|
|
57
|
+
if (!_config) throw new Error('Config not loaded');
|
|
58
|
+
return _config;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function saveConfig(root, config) {
|
|
62
|
+
const path = join(root, CONFIG_FILE);
|
|
63
|
+
await mkdir(dirname(path), { recursive: true });
|
|
64
|
+
const { _root, _path, ...clean } = config;
|
|
65
|
+
await writeFile(path, JSON.stringify(clean, null, 2), 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { expand };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confirm โ ask before destructive operations
|
|
3
|
+
* Pattern: Claude Code asks before rm, force push, overwrite, etc.
|
|
4
|
+
*/
|
|
5
|
+
import { createInterface } from 'readline';
|
|
6
|
+
import { COLORS } from './logger.js';
|
|
7
|
+
|
|
8
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
9
|
+
{ pattern: /delete|remove|rm/i, level: 'warn', msg: 'This will delete files' },
|
|
10
|
+
{ pattern: /force.*push|push.*force/i, level: 'danger', msg: 'Force push will overwrite remote' },
|
|
11
|
+
{ pattern: /reset.*--hard/i, level: 'danger', msg: 'Hard reset will lose changes' },
|
|
12
|
+
{ pattern: /checkout.*--force/i, level: 'warn', msg: 'Force checkout will discard changes' },
|
|
13
|
+
{ pattern: /drop.*table|truncate/i, level: 'danger', msg: 'Database destructive operation' },
|
|
14
|
+
{ pattern: /chmod.*777/i, level: 'warn', msg: 'Insecure permissions' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function isDestructive(toolName, args) {
|
|
18
|
+
const str = JSON.stringify(args);
|
|
19
|
+
for (const { pattern, level, msg } of DESTRUCTIVE_PATTERNS) {
|
|
20
|
+
if (pattern.test(str)) return { destructive: true, level, msg };
|
|
21
|
+
}
|
|
22
|
+
if (toolName === 'file' && args.action === 'delete') return { destructive: true, level: 'warn', msg: 'Delete file' };
|
|
23
|
+
if (toolName === 'shell' && /rm\s/.test(args.command)) return { destructive: true, level: 'warn', msg: 'Remove files' };
|
|
24
|
+
return { destructive: false };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function askConfirmation(toolName, args, check) {
|
|
28
|
+
if (!check.destructive) return true;
|
|
29
|
+
|
|
30
|
+
const color = check.level === 'danger' ? COLORS.red : COLORS.yellow;
|
|
31
|
+
const icon = check.level === 'danger' ? '๐จ' : 'โ ๏ธ';
|
|
32
|
+
|
|
33
|
+
console.log(`\n${color}${icon} ${check.msg}${COLORS.reset}`);
|
|
34
|
+
console.log(`${COLORS.dim} Tool: ${toolName}${COLORS.reset}`);
|
|
35
|
+
console.log(`${COLORS.dim} Args: ${JSON.stringify(args).slice(0, 100)}${COLORS.reset}`);
|
|
36
|
+
|
|
37
|
+
if (!process.stdin.isTTY) {
|
|
38
|
+
console.log(`${COLORS.yellow} Auto-approved (non-interactive)${COLORS.reset}\n`);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
rl.question(`${color} Continue? (y/N): ${COLORS.reset}`, (answer) => {
|
|
45
|
+
rl.close();
|
|
46
|
+
const ok = answer.trim().toLowerCase() === 'y';
|
|
47
|
+
if (!ok) console.log(`${COLORS.red} โ Cancelled${COLORS.reset}\n`);
|
|
48
|
+
resolve(ok);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context โ inject workspace context into prompts
|
|
3
|
+
* Like Claude Code: auto-detect project, git status, recent files
|
|
4
|
+
*/
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { readFile } from 'fs/promises';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { getConfig } from './config.js';
|
|
10
|
+
|
|
11
|
+
function run(cmd, cwd, timeout = 5000) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
exec(cmd, { cwd, timeout, maxBuffer: 256 * 1024 }, (_, stdout) => resolve(stdout?.trim() || ''));
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function buildContext() {
|
|
18
|
+
const cfg = getConfig();
|
|
19
|
+
const cwd = cfg.agent?.workspace || process.cwd();
|
|
20
|
+
const lines = [];
|
|
21
|
+
|
|
22
|
+
// Git context
|
|
23
|
+
if (existsSync(join(cwd, '.git'))) {
|
|
24
|
+
const branch = await run('git rev-parse --abbrev-ref HEAD', cwd);
|
|
25
|
+
const status = await run('git status --porcelain', cwd);
|
|
26
|
+
const lastCommit = await run('git log --oneline -1', cwd);
|
|
27
|
+
lines.push(`Git: branch=${branch}, ${status.split('\n').filter(Boolean).length} changes`);
|
|
28
|
+
if (lastCommit) lines.push(`Last commit: ${lastCommit}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Project context
|
|
32
|
+
const type = await detectType(cwd);
|
|
33
|
+
lines.push(`Project: ${type}`);
|
|
34
|
+
|
|
35
|
+
if (type === 'node') {
|
|
36
|
+
try {
|
|
37
|
+
const pkg = JSON.parse(await readFile(join(cwd, 'package.json'), 'utf-8'));
|
|
38
|
+
lines.push(`Package: ${pkg.name}@${pkg.version}`);
|
|
39
|
+
if (pkg.scripts) lines.push(`Scripts: ${Object.keys(pkg.scripts).join(', ')}`);
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Node/npm versions
|
|
44
|
+
const nodeVer = await run('node --version', cwd);
|
|
45
|
+
const npmVer = await run('npm --version', cwd);
|
|
46
|
+
lines.push(`Runtime: node=${nodeVer}, npm=${npmVer}`);
|
|
47
|
+
|
|
48
|
+
return lines.join('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function detectType(cwd) {
|
|
52
|
+
const checks = [
|
|
53
|
+
['package.json', 'node'], ['requirements.txt', 'python'], ['pyproject.toml', 'python'],
|
|
54
|
+
['Cargo.toml', 'rust'], ['go.mod', 'go'], ['CMakeLists.txt', 'cmake'],
|
|
55
|
+
['Makefile', 'make'], ['pom.xml', 'java'], ['build.gradle', 'gradle'],
|
|
56
|
+
];
|
|
57
|
+
for (const [file, type] of checks) {
|
|
58
|
+
if (existsSync(join(cwd, file))) return type;
|
|
59
|
+
}
|
|
60
|
+
return 'unknown';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function getWorkspaceSummary() {
|
|
64
|
+
const cfg = getConfig();
|
|
65
|
+
const cwd = cfg.agent?.workspace || process.cwd();
|
|
66
|
+
const type = await detectType(cwd);
|
|
67
|
+
const git = existsSync(join(cwd, '.git'));
|
|
68
|
+
return { path: cwd, type, hasGit: git };
|
|
69
|
+
}
|