leonai-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 (3) hide show
  1. package/README.md +61 -0
  2. package/cli.js +345 -0
  3. package/package.json +22 -0
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # ✦ LeonAI CLI
2
+
3
+ AI coding agent for your terminal. Create, edit, debug, and deploy — all from the command line.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g leonai-cli
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ ```bash
14
+ leonai login YOUR_API_KEY
15
+ ```
16
+
17
+ Get your API key at [leonai.dev](https://leonai.dev)
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ leonai
23
+ ```
24
+
25
+ ### Commands
26
+
27
+ | Command | Description |
28
+ |---------|-------------|
29
+ | `leonai` | Start interactive session |
30
+ | `leonai login <key>` | Save API key |
31
+ | `leonai logout` | Remove API key |
32
+ | `leonai config` | Show config |
33
+
34
+ ### In-session commands
35
+
36
+ | Command | Description |
37
+ |---------|-------------|
38
+ | `/run <cmd>` | Execute shell command |
39
+ | `/read <file>` | Read file into context |
40
+ | `/edit <file>` | AI edits a file |
41
+ | `/debug <file>` | Find and fix bugs |
42
+ | `/create` | Create a new project |
43
+ | `/tree` | Show directory structure |
44
+ | `/auto` | Toggle auto-execute |
45
+ | `/clear` | Clear conversation |
46
+ | `/quit` | Exit |
47
+
48
+ ## Examples
49
+
50
+ ```bash
51
+ ❯ buatin REST API express dengan JWT auth
52
+ 📄 Created: package.json
53
+ 📄 Created: server.js
54
+ 📄 Created: routes/auth.js
55
+ $ npm install && node server.js
56
+ ✓ Server running on port 3000
57
+ ```
58
+
59
+ ## License
60
+
61
+ MIT
package/cli.js ADDED
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env node
2
+ const readline = require('readline');
3
+ const https = require('https');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { execSync, spawnSync } = require('child_process');
7
+
8
+ // ===== COLORS =====
9
+ const c = {
10
+ r: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
11
+ green: '\x1b[32m', cyan: '\x1b[36m', yellow: '\x1b[33m',
12
+ red: '\x1b[31m', magenta: '\x1b[35m', blue: '\x1b[34m',
13
+ bg: '\x1b[48;5;236m'
14
+ };
15
+
16
+ // ===== CONFIG =====
17
+ const API_URL = 'https://inference.do-ai.run/v1/chat/completions';
18
+ const MODEL = 'deepseek-v4-pro';
19
+ const CWD = process.cwd();
20
+ const CONFIG_DIR = path.join(require('os').homedir(), '.leonai');
21
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
22
+
23
+ // Load or create config
24
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
25
+ function loadConfig() { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch(e) { return {}; } }
26
+ function saveConfig(cfg) { fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2)); }
27
+
28
+ let config = loadConfig();
29
+
30
+ // Handle CLI args
31
+ const args = process.argv.slice(2);
32
+ if (args[0] === 'login') {
33
+ const key = args[1];
34
+ if (!key) { console.log('Usage: leonai login <YOUR_API_KEY>'); process.exit(1); }
35
+ config.api_key = key;
36
+ saveConfig(config);
37
+ console.log(`${c.green}✓ API key saved! You're ready to go.${c.r}`);
38
+ console.log(`${c.dim} Run 'leonai' to start chatting.${c.r}`);
39
+ process.exit(0);
40
+ }
41
+ if (args[0] === 'logout') {
42
+ delete config.api_key;
43
+ saveConfig(config);
44
+ console.log('Logged out. Run "leonai login <key>" to re-authenticate.');
45
+ process.exit(0);
46
+ }
47
+ if (args[0] === 'config') {
48
+ console.log(`Config: ${CONFIG_FILE}`);
49
+ console.log(`API Key: ${config.api_key ? config.api_key.slice(0, 12) + '...' : '(not set)'}`);
50
+ console.log(`Model: ${MODEL}`);
51
+ process.exit(0);
52
+ }
53
+
54
+ const API_KEY = config.api_key || process.env.LEONAI_KEY || '';
55
+ if (!API_KEY) {
56
+ console.log(`\n${c.bold}${c.green} ✦ LeonAI CLI${c.r}\n`);
57
+ console.log(`${c.yellow} You need an API key to use LeonAI.${c.r}\n`);
58
+ console.log(` 1. Get your key at ${c.cyan}https://leonai.dev${c.r}`);
59
+ console.log(` 2. Run: ${c.green}leonai login <YOUR_API_KEY>${c.r}\n`);
60
+ console.log(` Or set env: ${c.dim}export LEONAI_KEY=your_key${c.r}\n`);
61
+ process.exit(1);
62
+ }
63
+
64
+ // ===== STATE =====
65
+ const messages = [];
66
+ let autoExec = true;
67
+
68
+ const SYSTEM = `You are LeonAI, a powerful AI coding agent running in the user's terminal at: ${CWD}
69
+
70
+ ## CAPABILITIES:
71
+ - Read, create, edit, delete files
72
+ - Run shell commands
73
+ - Debug code
74
+ - Build full projects
75
+ - Analyze codebases
76
+
77
+ ## OUTPUT FORMAT:
78
+ When you need to perform actions, use these EXACT markers:
79
+
80
+ To create/write a file:
81
+ <<<FILE:path/to/file>>>
82
+ content here
83
+ <<<END>>>
84
+
85
+ To run a command:
86
+ <<<RUN>>>
87
+ command here
88
+ <<<END>>>
89
+
90
+ To edit a specific part of a file (find and replace):
91
+ <<<EDIT:path/to/file>>>
92
+ <<<OLD>>>
93
+ old content
94
+ <<<NEW>>>
95
+ new content
96
+ <<<END>>>
97
+
98
+ ## RULES:
99
+ - ALWAYS use the markers above when creating files or running commands. Never just show code without markers.
100
+ - Be concise. Minimal explanation, maximum action.
101
+ - Match user's language.
102
+ - When user says "buatin/bikin/create" → create the files immediately.
103
+ - When debugging → read the file, find bugs, fix with EDIT markers.
104
+ - You can chain multiple FILE and RUN blocks in one response.
105
+ - Current directory: ${CWD}
106
+ - OS: ${process.platform}
107
+ - Available: node, python3, git, bash`;
108
+
109
+ messages.push({ role: 'system', content: SYSTEM });
110
+
111
+ // ===== UI =====
112
+ function print(text) { process.stdout.write(text); }
113
+ function println(text = '') { console.log(text); }
114
+ function header() {
115
+ println(`\n${c.bold}${c.green} ✦ LeonAI CLI ${c.r}${c.dim}v1.0.0${c.r}`);
116
+ println(`${c.dim} Working in: ${CWD}${c.r}`);
117
+ println(`${c.dim} Commands: /help /run /read /clear /auto /quit${c.r}\n`);
118
+ }
119
+
120
+ function showHelp() {
121
+ println(`${c.bold}Commands:${c.r}`);
122
+ println(` ${c.cyan}/run <cmd>${c.r} Run a shell command`);
123
+ println(` ${c.cyan}/read <file>${c.r} Read file and add to context`);
124
+ println(` ${c.cyan}/edit <file>${c.r} Ask AI to edit a file`);
125
+ println(` ${c.cyan}/debug <file>${c.r} Debug a file`);
126
+ println(` ${c.cyan}/create${c.r} Ask AI to create a project`);
127
+ println(` ${c.cyan}/tree${c.r} Show directory structure`);
128
+ println(` ${c.cyan}/auto${c.r} Toggle auto-execute (${autoExec ? 'ON' : 'OFF'})`);
129
+ println(` ${c.cyan}/clear${c.r} Clear conversation`);
130
+ println(` ${c.cyan}/quit${c.r} Exit`);
131
+ println();
132
+ }
133
+
134
+ function tree(dir, prefix = '', depth = 0) {
135
+ if (depth > 3) return '';
136
+ let out = '';
137
+ const items = fs.readdirSync(dir).filter(n => !n.startsWith('.') && n !== 'node_modules' && n !== '__pycache__');
138
+ items.forEach((item, i) => {
139
+ const fp = path.join(dir, item);
140
+ const isLast = i === items.length - 1;
141
+ const connector = isLast ? '└── ' : '├── ';
142
+ const stat = fs.statSync(fp);
143
+ if (stat.isDirectory()) {
144
+ out += `${prefix}${connector}${c.cyan}${item}/${c.r}\n`;
145
+ out += tree(fp, prefix + (isLast ? ' ' : '│ '), depth + 1);
146
+ } else {
147
+ out += `${prefix}${connector}${item}\n`;
148
+ }
149
+ });
150
+ return out;
151
+ }
152
+
153
+ // ===== EXECUTION ENGINE =====
154
+ function executeResponse(text) {
155
+ let actions = 0;
156
+
157
+ // FILE blocks
158
+ const fileRegex = /<<<FILE:(.+?)>>>\n([\s\S]*?)<<<END>>>/g;
159
+ let m;
160
+ while ((m = fileRegex.exec(text)) !== null) {
161
+ const fp = m[1].trim();
162
+ const content = m[2];
163
+ const dir = path.dirname(fp);
164
+ if (dir !== '.') fs.mkdirSync(dir, { recursive: true });
165
+ fs.writeFileSync(fp, content);
166
+ println(` ${c.yellow}📄 Created: ${fp}${c.r}`);
167
+ actions++;
168
+ }
169
+
170
+ // EDIT blocks
171
+ const editRegex = /<<<EDIT:(.+?)>>>\n<<<OLD>>>\n([\s\S]*?)<<<NEW>>>\n([\s\S]*?)<<<END>>>/g;
172
+ while ((m = editRegex.exec(text)) !== null) {
173
+ const fp = m[1].trim();
174
+ const old = m[2].trimEnd();
175
+ const newContent = m[3].trimEnd();
176
+ if (fs.existsSync(fp)) {
177
+ let file = fs.readFileSync(fp, 'utf8');
178
+ if (file.includes(old)) {
179
+ file = file.replace(old, newContent);
180
+ fs.writeFileSync(fp, file);
181
+ println(` ${c.yellow}✏️ Edited: ${fp}${c.r}`);
182
+ actions++;
183
+ } else {
184
+ println(` ${c.red}⚠️ Could not find text to replace in ${fp}${c.r}`);
185
+ }
186
+ } else {
187
+ println(` ${c.red}⚠️ File not found: ${fp}${c.r}`);
188
+ }
189
+ }
190
+
191
+ // RUN blocks
192
+ const runRegex = /<<<RUN>>>\n([\s\S]*?)<<<END>>>/g;
193
+ while ((m = runRegex.exec(text)) !== null) {
194
+ const cmd = m[1].trim();
195
+ println(` ${c.dim}$ ${cmd}${c.r}`);
196
+ try {
197
+ const out = execSync(cmd, { encoding: 'utf8', timeout: 30000, cwd: CWD, maxBuffer: 2 * 1024 * 1024 });
198
+ if (out.trim()) println(` ${c.green}${out.trim()}${c.r}`);
199
+ actions++;
200
+ } catch (e) {
201
+ println(` ${c.red}${(e.stderr || e.stdout || e.message).trim()}${c.r}`);
202
+ actions++;
203
+ }
204
+ }
205
+
206
+ if (actions > 0) println();
207
+ return actions;
208
+ }
209
+
210
+ // ===== API =====
211
+ function callAPI(msgs) {
212
+ return new Promise((resolve, reject) => {
213
+ const data = JSON.stringify({ model: MODEL, messages: msgs, max_tokens: 4096 });
214
+ const url = new URL(API_URL);
215
+ const req = https.request({
216
+ hostname: url.hostname, path: url.pathname, method: 'POST',
217
+ headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + API_KEY, 'Content-Length': Buffer.byteLength(data) }
218
+ }, res => {
219
+ let body = '';
220
+ res.on('data', c => body += c);
221
+ res.on('end', () => {
222
+ try {
223
+ const json = JSON.parse(body);
224
+ if (json.error) return reject(json.error.message || JSON.stringify(json.error));
225
+ const msg = json.choices?.[0]?.message;
226
+ resolve(msg?.content || msg?.reasoning_content || 'No response');
227
+ } catch (e) { reject(body); }
228
+ });
229
+ });
230
+ req.on('error', reject);
231
+ req.write(data);
232
+ req.end();
233
+ });
234
+ }
235
+
236
+ // ===== CHAT =====
237
+ async function chat(input) {
238
+ messages.push({ role: 'user', content: input });
239
+ print(`\n${c.green}✦ LeonAI${c.r} ${c.dim}thinking...${c.r}`);
240
+
241
+ try {
242
+ const reply = await callAPI(messages);
243
+ // Clear "thinking..." line
244
+ if (process.stdout.isTTY) { process.stdout.clearLine(0); process.stdout.cursorTo(0); }
245
+ else println();
246
+
247
+ // Print reply (strip action markers for display)
248
+ const display = reply
249
+ .replace(/<<<FILE:.+?>>>\n[\s\S]*?<<<END>>>/g, '')
250
+ .replace(/<<<EDIT:.+?>>>\n[\s\S]*?<<<END>>>/g, '')
251
+ .replace(/<<<RUN>>>\n[\s\S]*?<<<END>>>/g, '')
252
+ .trim();
253
+
254
+ if (display) println(`${c.green}✦ LeonAI:${c.r} ${display}\n`);
255
+
256
+ messages.push({ role: 'assistant', content: reply });
257
+
258
+ // Execute actions
259
+ if (autoExec) {
260
+ executeResponse(reply);
261
+ } else {
262
+ const hasActions = /<<<(FILE|RUN|EDIT)/.test(reply);
263
+ if (hasActions) {
264
+ println(`${c.yellow} Actions detected. Run /exec to execute.${c.r}\n`);
265
+ }
266
+ }
267
+ } catch (e) {
268
+ if (process.stdout.isTTY) { process.stdout.clearLine?.(0); process.stdout.cursorTo?.(0); }
269
+ println(`${c.red}Error: ${e}${c.r}\n`);
270
+ }
271
+ }
272
+
273
+ // ===== MAIN LOOP =====
274
+ header();
275
+
276
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
277
+
278
+ function prompt() {
279
+ rl.question(`${c.cyan}❯ ${c.r}`, async (input) => {
280
+ input = input.trim();
281
+ if (!input) return prompt();
282
+
283
+ // Commands
284
+ if (input === '/quit' || input === '/exit' || input === '/q') { println('👋 Bye!'); process.exit(0); }
285
+ if (input === '/clear') { messages.length = 1; println(`${c.dim}Conversation cleared.${c.r}\n`); return prompt(); }
286
+ if (input === '/help' || input === '/?') { showHelp(); return prompt(); }
287
+ if (input === '/auto') { autoExec = !autoExec; println(`${c.dim}Auto-execute: ${autoExec ? 'ON' : 'OFF'}${c.r}\n`); return prompt(); }
288
+ if (input === '/tree') { println(tree(CWD)); return prompt(); }
289
+ if (input === '/exec') {
290
+ const last = messages[messages.length - 1];
291
+ if (last?.role === 'assistant') executeResponse(last.content);
292
+ return prompt();
293
+ }
294
+
295
+ if (input.startsWith('/run ')) {
296
+ const cmd = input.slice(5);
297
+ println(`${c.dim}$ ${cmd}${c.r}`);
298
+ try { const out = execSync(cmd, { encoding: 'utf8', timeout: 30000, cwd: CWD }); if (out.trim()) println(out.trim()); } catch (e) { println(`${c.red}${e.stderr || e.message}${c.r}`); }
299
+ println();
300
+ return prompt();
301
+ }
302
+
303
+ if (input.startsWith('/read ')) {
304
+ const fp = input.slice(6);
305
+ if (fs.existsSync(fp)) {
306
+ const content = fs.readFileSync(fp, 'utf8');
307
+ println(`${c.dim}Read ${fp} (${content.length} chars)${c.r}\n`);
308
+ messages.push({ role: 'user', content: `[File: ${fp}]\n${content}` });
309
+ } else { println(`${c.red}File not found: ${fp}${c.r}\n`); }
310
+ return prompt();
311
+ }
312
+
313
+ if (input.startsWith('/edit ')) {
314
+ const fp = input.slice(6);
315
+ if (fs.existsSync(fp)) {
316
+ const content = fs.readFileSync(fp, 'utf8');
317
+ await chat(`Edit this file (${fp}):\n\`\`\`\n${content}\n\`\`\`\nWhat should I change?`);
318
+ } else { println(`${c.red}File not found: ${fp}${c.r}\n`); }
319
+ return prompt();
320
+ }
321
+
322
+ if (input.startsWith('/debug ')) {
323
+ const fp = input.slice(7);
324
+ if (fs.existsSync(fp)) {
325
+ const content = fs.readFileSync(fp, 'utf8');
326
+ await chat(`Debug this file (${fp}). Find all bugs and fix them:\n\`\`\`\n${content}\n\`\`\``);
327
+ } else { println(`${c.red}File not found: ${fp}${c.r}\n`); }
328
+ return prompt();
329
+ }
330
+
331
+ if (input === '/create') {
332
+ rl.question(`${c.dim}What to create? ${c.r}`, async (desc) => {
333
+ if (desc.trim()) await chat(`Create this project: ${desc}`);
334
+ prompt();
335
+ });
336
+ return;
337
+ }
338
+
339
+ // Regular chat
340
+ await chat(input);
341
+ prompt();
342
+ });
343
+ }
344
+
345
+ prompt();
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "leonai-cli",
3
+ "version": "1.0.0",
4
+ "description": "AI coding agent for your terminal — create, edit, debug, and deploy from the command line",
5
+ "bin": {
6
+ "leonai": "./cli.js"
7
+ },
8
+ "files": [
9
+ "cli.js"
10
+ ],
11
+ "keywords": ["ai", "cli", "coding", "agent", "terminal", "gpt", "copilot", "developer-tools"],
12
+ "author": "LeonAI",
13
+ "license": "MIT",
14
+ "engines": {
15
+ "node": ">=16.0.0"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/leonai/cli"
20
+ },
21
+ "homepage": "https://leonai.dev"
22
+ }