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.
Files changed (56) hide show
  1. package/README.md +273 -0
  2. package/config.example.json +24 -0
  3. package/package.json +44 -0
  4. package/src/core/auto-format.js +47 -0
  5. package/src/core/completion.js +65 -0
  6. package/src/core/config.js +68 -0
  7. package/src/core/confirm.js +51 -0
  8. package/src/core/context.js +69 -0
  9. package/src/core/conversation.js +57 -0
  10. package/src/core/diff.js +50 -0
  11. package/src/core/doctor.js +60 -0
  12. package/src/core/error-handler.js +39 -0
  13. package/src/core/event-bus.js +43 -0
  14. package/src/core/file-tracker.js +44 -0
  15. package/src/core/formatter.js +83 -0
  16. package/src/core/intent.js +81 -0
  17. package/src/core/keybindings.js +29 -0
  18. package/src/core/logger.js +67 -0
  19. package/src/core/model-info.js +26 -0
  20. package/src/core/retry.js +26 -0
  21. package/src/core/sandbox.js +54 -0
  22. package/src/core/session.js +46 -0
  23. package/src/core/spinner.js +60 -0
  24. package/src/core/stats.js +73 -0
  25. package/src/core/system-prompt.js +42 -0
  26. package/src/core/thinking.js +39 -0
  27. package/src/core/tool-help.js +117 -0
  28. package/src/core/tool-registry.js +82 -0
  29. package/src/core/wizard.js +55 -0
  30. package/src/core/workspace.js +96 -0
  31. package/src/index.js +150 -0
  32. package/src/mcp/activation.js +54 -0
  33. package/src/mcp/mcp-handler.js +92 -0
  34. package/src/mcp/message-sender.js +147 -0
  35. package/src/mcp/mqtt-client.js +160 -0
  36. package/src/mcp/wss-endpoint.js +133 -0
  37. package/src/plugins/manager.js +72 -0
  38. package/src/repl-commands.js +91 -0
  39. package/src/repl.js +115 -0
  40. package/src/tools/ai.js +128 -0
  41. package/src/tools/config.js +87 -0
  42. package/src/tools/diff.js +118 -0
  43. package/src/tools/env.js +58 -0
  44. package/src/tools/file.js +177 -0
  45. package/src/tools/git.js +166 -0
  46. package/src/tools/history.js +64 -0
  47. package/src/tools/network.js +87 -0
  48. package/src/tools/process.js +69 -0
  49. package/src/tools/project.js +152 -0
  50. package/src/tools/search.js +100 -0
  51. package/src/tools/shell.js +91 -0
  52. package/src/tools/snippet.js +95 -0
  53. package/src/tools/template.js +113 -0
  54. package/src/tools/watch.js +87 -0
  55. package/src/tui/renderer.js +119 -0
  56. 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
+ }