shiroai 1.0.0 → 2.0.1

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 +29 -22
  2. package/cli.js +497 -285
  3. package/package.json +6 -10
package/README.md CHANGED
@@ -1,54 +1,61 @@
1
- # ✦ LeonAI CLI
1
+ # ✦ ShiroAI CLI
2
2
 
3
- AI coding agent for your terminal. Create, edit, debug, and deployall from the command line.
3
+ AI coding agent for your terminal. Reads, writes, edits files, runs commandslike having a senior dev beside you.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install -g leonai-cli
8
+ npm install -g shiroai
9
9
  ```
10
10
 
11
11
  ## Setup
12
12
 
13
13
  ```bash
14
- leonai login YOUR_API_KEY
14
+ shiroai login YOUR_API_KEY
15
15
  ```
16
16
 
17
- Get your API key at [leonai.dev](https://leonai.dev)
17
+ Get your API key at [shiroai.dev](https://shiroai.dev)
18
18
 
19
19
  ## Usage
20
20
 
21
21
  ```bash
22
- leonai
22
+ shiroai
23
23
  ```
24
24
 
25
- ### Commands
25
+ ## Features
26
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 |
27
+ - **Tool Use** AI autonomously reads, writes, edits files and runs commands
28
+ - **Streaming** — Token-by-token output, see responses as they generate
29
+ - **Context Aware** Auto-detects your project structure
30
+ - **Sessions** Save/load conversations
31
+ - **Agents** Switch between default, plan, or custom agents
33
32
 
34
- ### In-session commands
33
+ ## Commands
35
34
 
36
35
  | Command | Description |
37
36
  |---------|-------------|
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 |
37
+ | `/help` | Show all commands |
38
+ | `/context` | Show context info |
39
+ | `/context add <file>` | Add file to context |
40
+ | `/chat save <name>` | Save session |
41
+ | `/chat load <name>` | Load session |
42
+ | `/chat list` | List sessions |
43
+ | `/agent list` | List agents |
44
+ | `/agent <name>` | Switch agent |
45
+ | `/plan` | Planning mode |
45
46
  | `/clear` | Clear conversation |
46
47
  | `/quit` | Exit |
47
48
 
48
- ## Examples
49
+ ## Example
49
50
 
50
51
  ```bash
52
+ ❯ shiroai
53
+ ✦ ShiroAI v2.0.0
54
+ Agent: default | Model: deepseek-v4-pro
55
+
51
56
  ❯ buatin REST API express dengan JWT auth
57
+
58
+ ─── tools ───
52
59
  📄 Created: package.json
53
60
  📄 Created: server.js
54
61
  📄 Created: routes/auth.js
package/cli.js CHANGED
@@ -1,345 +1,557 @@
1
1
  #!/usr/bin/env node
2
2
  const readline = require('readline');
3
3
  const https = require('https');
4
+ const http = require('http');
4
5
  const fs = require('fs');
5
6
  const path = require('path');
6
- const { execSync, spawnSync } = require('child_process');
7
+ const { execSync, spawn } = require('child_process');
8
+ const os = require('os');
7
9
 
8
10
  // ===== COLORS =====
9
11
  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'
12
+ r: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', italic: '\x1b[3m',
13
+ green: '\x1b[32m', cyan: '\x1b[36m', yellow: '\x1b[33m',
14
+ red: '\x1b[31m', magenta: '\x1b[35m', blue: '\x1b[34m', white: '\x1b[37m',
15
+ bgGreen: '\x1b[42m', bgBlue: '\x1b[44m', bgMagenta: '\x1b[45m',
14
16
  };
15
17
 
16
18
  // ===== CONFIG =====
17
- const API_URL = 'https://inference.do-ai.run/v1/chat/completions';
19
+ const SERVER_URL = process.env.SHIROAI_SERVER || 'http://157.230.42.115:3000';
18
20
  const MODEL = 'deepseek-v4-pro';
19
21
  const CWD = process.cwd();
20
- const CONFIG_DIR = path.join(require('os').homedir(), '.shiroai');
22
+ const CONFIG_DIR = path.join(os.homedir(), '.shiroai');
21
23
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
24
+ const SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions');
25
+ const AGENTS_DIR = path.join(CONFIG_DIR, 'agents');
22
26
 
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
+ // Ensure dirs
28
+ [CONFIG_DIR, SESSIONS_DIR, AGENTS_DIR].forEach(d => {
29
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
30
+ });
27
31
 
32
+ function loadConfig() { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; } }
33
+ function saveConfig(cfg) { fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2)); }
28
34
  let config = loadConfig();
29
35
 
30
- // Handle CLI args
36
+ // ===== CLI ARGS =====
31
37
  const args = process.argv.slice(2);
32
38
  if (args[0] === 'login') {
33
- const key = args[1];
34
- if (!key) { console.log('Usage: shiroai 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 'shiroai' 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 "shiroai login <key>" to re-authenticate.');
45
- process.exit(0);
39
+ if (!args[1]) { console.log('Usage: shiroai login <API_KEY>'); process.exit(1); }
40
+ config.api_key = args[1]; saveConfig(config);
41
+ console.log(`${c.green}✓ API key saved!${c.r}`); process.exit(0);
46
42
  }
43
+ if (args[0] === 'logout') { delete config.api_key; saveConfig(config); console.log('Logged out.'); process.exit(0); }
47
44
  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);
45
+ console.log(`Config: ${CONFIG_FILE}\nAPI Key: ${config.api_key ? config.api_key.slice(0, 16) + '...' : '(not set)'}\nModel: ${MODEL}`);
46
+ process.exit(0);
52
47
  }
53
48
 
54
- const API_KEY = config.api_key || process.env.LEONAI_KEY || '';
49
+ const API_KEY = config.api_key || process.env.SHIROAI_KEY || '';
55
50
  if (!API_KEY) {
56
- console.log(`\n${c.bold}${c.green} ✦ ShiroAI CLI${c.r}\n`);
57
- console.log(`${c.yellow} You need an API key to use ShiroAI.${c.r}\n`);
58
- console.log(` 1. Get your key at ${c.cyan}https://shiroai.dev${c.r}`);
59
- console.log(` 2. Run: ${c.green}shiroai 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);
51
+ console.log(`\n${c.bold}${c.green} ✦ ShiroAI${c.r}\n\n ${c.yellow}API key required.${c.r}\n\n 1. Get key at ${c.cyan}https://shiroai.dev${c.r}\n 2. Run: ${c.green}shiroai login <KEY>${c.r}\n`);
52
+ process.exit(1);
62
53
  }
63
54
 
64
- // ===== STATE =====
65
- const messages = [];
66
- let autoExec = true;
67
-
68
- const SYSTEM = `You are ShiroAI, 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 });
55
+ // ===== TOOLS DEFINITION =====
56
+ const TOOLS = [
57
+ {
58
+ type: 'function',
59
+ function: {
60
+ name: 'read_file',
61
+ description: 'Read the contents of a file. Use this to understand code before making changes.',
62
+ parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path relative to CWD' } }, required: ['path'] }
63
+ }
64
+ },
65
+ {
66
+ type: 'function',
67
+ function: {
68
+ name: 'write_file',
69
+ description: 'Create or overwrite a file with content.',
70
+ parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path' }, content: { type: 'string', description: 'File content' } }, required: ['path', 'content'] }
71
+ }
72
+ },
73
+ {
74
+ type: 'function',
75
+ function: {
76
+ name: 'edit_file',
77
+ description: 'Edit a file by replacing old text with new text.',
78
+ parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path' }, old_text: { type: 'string', description: 'Text to find' }, new_text: { type: 'string', description: 'Replacement text' } }, required: ['path', 'old_text', 'new_text'] }
79
+ }
80
+ },
81
+ {
82
+ type: 'function',
83
+ function: {
84
+ name: 'run_command',
85
+ description: 'Execute a shell command and return output. Use for installing deps, running tests, git, etc.',
86
+ parameters: { type: 'object', properties: { command: { type: 'string', description: 'Shell command to execute' } }, required: ['command'] }
87
+ }
88
+ },
89
+ {
90
+ type: 'function',
91
+ function: {
92
+ name: 'search_files',
93
+ description: 'Search for text pattern in files recursively. Returns matching lines with file paths.',
94
+ parameters: { type: 'object', properties: { pattern: { type: 'string', description: 'Text or regex pattern' }, path: { type: 'string', description: 'Directory to search in (default: .)' } }, required: ['pattern'] }
95
+ }
96
+ },
97
+ {
98
+ type: 'function',
99
+ function: {
100
+ name: 'list_directory',
101
+ description: 'List files and directories. Use to understand project structure.',
102
+ parameters: { type: 'object', properties: { path: { type: 'string', description: 'Directory path (default: .)' }, recursive: { type: 'boolean', description: 'List recursively (default: false)' } }, required: [] }
103
+ }
104
+ }
105
+ ];
106
+
107
+ // ===== TOOL EXECUTION =====
108
+ function executeTool(name, args) {
109
+ try {
110
+ switch (name) {
111
+ case 'read_file': {
112
+ const fp = path.resolve(CWD, args.path);
113
+ if (!fs.existsSync(fp)) return { error: `File not found: ${args.path}` };
114
+ const content = fs.readFileSync(fp, 'utf8');
115
+ process.stdout.write(` ${c.dim}📖 Read: ${args.path} (${content.length} chars)${c.r}\n`);
116
+ return { content };
117
+ }
118
+ case 'write_file': {
119
+ const fp = path.resolve(CWD, args.path);
120
+ const dir = path.dirname(fp);
121
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
122
+ fs.writeFileSync(fp, args.content);
123
+ process.stdout.write(` ${c.yellow}📄 Created: ${args.path}${c.r}\n`);
124
+ return { success: true };
125
+ }
126
+ case 'edit_file': {
127
+ const fp = path.resolve(CWD, args.path);
128
+ if (!fs.existsSync(fp)) return { error: `File not found: ${args.path}` };
129
+ let content = fs.readFileSync(fp, 'utf8');
130
+ if (!content.includes(args.old_text)) return { error: 'Old text not found in file' };
131
+ content = content.replace(args.old_text, args.new_text);
132
+ fs.writeFileSync(fp, content);
133
+ process.stdout.write(` ${c.yellow}✏️ Edited: ${args.path}${c.r}\n`);
134
+ return { success: true };
135
+ }
136
+ case 'run_command': {
137
+ process.stdout.write(` ${c.dim}$ ${args.command}${c.r}\n`);
138
+ try {
139
+ const out = execSync(args.command, { encoding: 'utf8', timeout: 60000, cwd: CWD, maxBuffer: 5 * 1024 * 1024 });
140
+ const trimmed = out.trim();
141
+ if (trimmed) process.stdout.write(` ${c.green}${trimmed.split('\n').slice(0, 20).join('\n ')}${c.r}\n`);
142
+ return { output: trimmed };
143
+ } catch (e) {
144
+ const err = (e.stderr || e.stdout || e.message).trim();
145
+ process.stdout.write(` ${c.red}${err.split('\n').slice(0, 10).join('\n ')}${c.r}\n`);
146
+ return { error: err };
147
+ }
148
+ }
149
+ case 'search_files': {
150
+ const dir = path.resolve(CWD, args.path || '.');
151
+ try {
152
+ const out = execSync(`grep -rn --include="*" "${args.pattern}" "${dir}" 2>/dev/null | head -30`, { encoding: 'utf8', timeout: 10000 });
153
+ process.stdout.write(` ${c.dim}🔍 Search: "${args.pattern}"${c.r}\n`);
154
+ return { results: out.trim() || 'No matches found' };
155
+ } catch { return { results: 'No matches found' }; }
156
+ }
157
+ case 'list_directory': {
158
+ const dir = path.resolve(CWD, args.path || '.');
159
+ if (!fs.existsSync(dir)) return { error: `Directory not found: ${args.path}` };
160
+ const items = listDir(dir, args.recursive ? 2 : 0);
161
+ process.stdout.write(` ${c.dim}📂 Listed: ${args.path || '.'}${c.r}\n`);
162
+ return { files: items };
163
+ }
164
+ default: return { error: `Unknown tool: ${name}` };
165
+ }
166
+ } catch (e) { return { error: e.message }; }
167
+ }
110
168
 
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} ShiroAI 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`);
169
+ function listDir(dir, depth, prefix = '') {
170
+ if (depth < 0) return [];
171
+ const items = [];
172
+ const entries = fs.readdirSync(dir).filter(n => !n.startsWith('.') && n !== 'node_modules' && n !== '__pycache__' && n !== 'dist' && n !== '.git');
173
+ for (const entry of entries.slice(0, 50)) {
174
+ const fp = path.join(dir, entry);
175
+ const stat = fs.statSync(fp);
176
+ if (stat.isDirectory()) {
177
+ items.push(prefix + entry + '/');
178
+ if (depth > 0) items.push(...listDir(fp, depth - 1, prefix + ' '));
179
+ } else {
180
+ items.push(prefix + entry);
181
+ }
182
+ }
183
+ return items;
118
184
  }
119
185
 
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();
186
+ // ===== AGENTS =====
187
+ const AGENTS = {
188
+ default: {
189
+ name: 'default',
190
+ description: 'Full-featured coding agent',
191
+ prompt: `You are ShiroAI, a powerful AI coding agent running in the user's terminal.
192
+ Working directory: ${CWD}
193
+ OS: ${process.platform}
194
+
195
+ You have access to tools to read, write, edit files, run commands, and search code.
196
+ Use tools proactively — read files before editing, check project structure before creating.
197
+ Be concise. Take action, don't just explain. Match user's language.`,
198
+ tools: TOOLS
199
+ },
200
+ plan: {
201
+ name: 'plan',
202
+ description: 'Planning agent — analyzes before acting',
203
+ prompt: `You are ShiroAI in PLANNING mode. You help users plan implementations before coding.
204
+ Working directory: ${CWD}
205
+
206
+ Your job:
207
+ 1. Ask clarifying questions about requirements
208
+ 2. Research the codebase using read_file, search_files, list_directory
209
+ 3. Create a detailed implementation plan with tasks
210
+ 4. DO NOT write or edit files — only read and analyze
211
+
212
+ Output a structured plan with numbered tasks when ready.`,
213
+ tools: TOOLS.filter(t => ['read_file', 'search_files', 'list_directory', 'run_command'].includes(t.function.name))
214
+ }
215
+ };
216
+
217
+ // Load custom agents
218
+ try {
219
+ const agentFiles = fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith('.json'));
220
+ for (const f of agentFiles) {
221
+ const agent = JSON.parse(fs.readFileSync(path.join(AGENTS_DIR, f), 'utf8'));
222
+ const name = path.basename(f, '.json');
223
+ AGENTS[name] = { ...agent, name, tools: agent.tools === 'all' ? TOOLS : TOOLS.filter(t => (agent.tools || []).includes(t.function.name)) };
224
+ }
225
+ } catch {}
226
+
227
+ // ===== STATE =====
228
+ let currentAgent = AGENTS.default;
229
+ let messages = [];
230
+ let contextFiles = [];
231
+ let sessionName = null;
232
+
233
+ function initMessages() {
234
+ messages = [{ role: 'system', content: currentAgent.prompt }];
235
+ // Auto-add project context
236
+ const contextInfo = getProjectContext();
237
+ if (contextInfo) messages.push({ role: 'system', content: `[Project Context]\n${contextInfo}` });
238
+ }
239
+
240
+ function getProjectContext() {
241
+ const parts = [];
242
+ // Check for common project files
243
+ const checks = ['package.json', 'Cargo.toml', 'pyproject.toml', 'go.mod', 'Makefile', 'docker-compose.yml'];
244
+ for (const f of checks) {
245
+ if (fs.existsSync(path.join(CWD, f))) {
246
+ try {
247
+ const content = fs.readFileSync(path.join(CWD, f), 'utf8');
248
+ parts.push(`[${f}]\n${content.slice(0, 2000)}`);
249
+ } catch {}
250
+ }
251
+ }
252
+ // Directory listing
253
+ try {
254
+ const items = listDir(CWD, 1);
255
+ parts.push(`[Directory Structure]\n${items.slice(0, 40).join('\n')}`);
256
+ } catch {}
257
+ return parts.join('\n\n');
132
258
  }
133
259
 
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`;
260
+ initMessages();
261
+
262
+ // ===== STREAMING API (via server proxy) =====
263
+ function streamChat(msgs, tools) {
264
+ return new Promise((resolve, reject) => {
265
+ const body = JSON.stringify({
266
+ model: MODEL,
267
+ messages: msgs,
268
+ tools: tools,
269
+ stream: true,
270
+ max_tokens: 8192
271
+ });
272
+ const url = new URL(SERVER_URL + '/api/chat');
273
+ const client = url.protocol === 'https:' ? https : http;
274
+
275
+ const req = client.request({
276
+ hostname: url.hostname,
277
+ port: url.port,
278
+ path: url.pathname,
279
+ method: 'POST',
280
+ headers: {
281
+ 'Content-Type': 'application/json',
282
+ 'X-API-Key': API_KEY,
283
+ 'Content-Length': Buffer.byteLength(body)
284
+ }
285
+ }, res => {
286
+ if (res.statusCode !== 200) {
287
+ let data = '';
288
+ res.on('data', c => data += c);
289
+ res.on('end', () => {
290
+ try { const j = JSON.parse(data); reject(j.error?.message || data); }
291
+ catch { reject(`HTTP ${res.statusCode}: ${data}`); }
292
+ });
293
+ return;
294
+ }
295
+
296
+ let fullContent = '';
297
+ let toolCalls = [];
298
+ let buffer = '';
299
+
300
+ res.on('data', chunk => {
301
+ buffer += chunk.toString();
302
+ const lines = buffer.split('\n');
303
+ buffer = lines.pop();
304
+
305
+ for (const line of lines) {
306
+ if (!line.startsWith('data: ')) continue;
307
+ const data = line.slice(6).trim();
308
+ if (data === '[DONE]') continue;
309
+
310
+ try {
311
+ const json = JSON.parse(data);
312
+ const delta = json.choices?.[0]?.delta;
313
+ if (!delta) continue;
314
+
315
+ // Streaming text
316
+ if (delta.content) {
317
+ fullContent += delta.content;
318
+ process.stdout.write(delta.content);
319
+ }
320
+
321
+ // Tool calls
322
+ if (delta.tool_calls) {
323
+ for (const tc of delta.tool_calls) {
324
+ if (tc.index !== undefined) {
325
+ if (!toolCalls[tc.index]) toolCalls[tc.index] = { id: '', function: { name: '', arguments: '' } };
326
+ if (tc.id) toolCalls[tc.index].id = tc.id;
327
+ if (tc.function?.name) toolCalls[tc.index].function.name = tc.function.name;
328
+ if (tc.function?.arguments) toolCalls[tc.index].function.arguments += tc.function.arguments;
329
+ }
330
+ }
331
+ }
332
+ } catch {}
148
333
  }
334
+ });
335
+
336
+ res.on('end', () => {
337
+ // Process remaining buffer
338
+ if (buffer) {
339
+ const lines = buffer.split('\n');
340
+ for (const line of lines) {
341
+ if (!line.startsWith('data: ')) continue;
342
+ const data = line.slice(6).trim();
343
+ if (data === '[DONE]') continue;
344
+ try {
345
+ const json = JSON.parse(data);
346
+ const delta = json.choices?.[0]?.delta;
347
+ if (delta?.content) { fullContent += delta.content; process.stdout.write(delta.content); }
348
+ if (delta?.tool_calls) {
349
+ for (const tc of delta.tool_calls) {
350
+ if (tc.index !== undefined) {
351
+ if (!toolCalls[tc.index]) toolCalls[tc.index] = { id: '', function: { name: '', arguments: '' } };
352
+ if (tc.id) toolCalls[tc.index].id = tc.id;
353
+ if (tc.function?.name) toolCalls[tc.index].function.name = tc.function.name;
354
+ if (tc.function?.arguments) toolCalls[tc.index].function.arguments += tc.function.arguments;
355
+ }
356
+ }
357
+ }
358
+ } catch {}
359
+ }
360
+ }
361
+ toolCalls = toolCalls.filter(Boolean);
362
+ resolve({ content: fullContent, tool_calls: toolCalls.length > 0 ? toolCalls : null });
363
+ });
149
364
  });
150
- return out;
365
+
366
+ req.on('error', reject);
367
+ req.write(body);
368
+ req.end();
369
+ });
151
370
  }
152
371
 
153
- // ===== EXECUTION ENGINE =====
154
- function executeResponse(text) {
155
- let actions = 0;
372
+ // ===== CHAT LOOP WITH TOOL USE =====
373
+ async function chat(input) {
374
+ messages.push({ role: 'user', content: input });
156
375
 
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
- }
376
+ process.stdout.write(`\n${c.green}✦ ShiroAI${c.r} ${c.dim}[${currentAgent.name}]${c.r}\n`);
169
377
 
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
- }
378
+ let iterations = 0;
379
+ const MAX_ITERATIONS = 10;
190
380
 
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
- }
381
+ while (iterations < MAX_ITERATIONS) {
382
+ iterations++;
383
+
384
+ try {
385
+ const result = await streamChat(messages, currentAgent.tools);
386
+
387
+ if (result.content) {
388
+ process.stdout.write('\n');
389
+ messages.push({ role: 'assistant', content: result.content });
390
+ }
391
+
392
+ if (!result.tool_calls) break;
393
+
394
+ // Process tool calls
395
+ const assistantMsg = { role: 'assistant', content: result.content || null, tool_calls: result.tool_calls };
396
+ if (!result.content) messages.push(assistantMsg);
397
+ else messages[messages.length - 1].tool_calls = result.tool_calls;
398
+
399
+ process.stdout.write(`\n${c.dim}─── tools ───${c.r}\n`);
400
+
401
+ for (const tc of result.tool_calls) {
402
+ let args;
403
+ try { args = JSON.parse(tc.function.arguments); } catch { args = {}; }
404
+
405
+ const toolResult = executeTool(tc.function.name, args);
406
+ messages.push({
407
+ role: 'tool',
408
+ tool_call_id: tc.id,
409
+ content: JSON.stringify(toolResult)
410
+ });
411
+ }
412
+
413
+ process.stdout.write(`${c.dim}─────────────${c.r}\n\n`);
414
+
415
+ } catch (e) {
416
+ process.stdout.write(`\n${c.red}Error: ${e}${c.r}\n`);
417
+ break;
204
418
  }
419
+ }
205
420
 
206
- if (actions > 0) println();
207
- return actions;
421
+ process.stdout.write('\n');
208
422
  }
209
423
 
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
- });
424
+ // ===== SESSION MANAGEMENT =====
425
+ function saveSession(name) {
426
+ const session = { name, agent: currentAgent.name, messages, contextFiles, timestamp: Date.now() };
427
+ fs.writeFileSync(path.join(SESSIONS_DIR, `${name}.json`), JSON.stringify(session));
428
+ process.stdout.write(`${c.green}✓ Session saved: ${name}${c.r}\n`);
234
429
  }
235
430
 
236
- // ===== CHAT =====
237
- async function chat(input) {
238
- messages.push({ role: 'user', content: input });
239
- print(`\n${c.green}✦ ShiroAI${c.r} ${c.dim}thinking...${c.r}`);
431
+ function loadSession(name) {
432
+ const fp = path.join(SESSIONS_DIR, `${name}.json`);
433
+ if (!fs.existsSync(fp)) { process.stdout.write(`${c.red}Session not found: ${name}${c.r}\n`); return; }
434
+ const session = JSON.parse(fs.readFileSync(fp, 'utf8'));
435
+ messages = session.messages;
436
+ contextFiles = session.contextFiles || [];
437
+ if (AGENTS[session.agent]) currentAgent = AGENTS[session.agent];
438
+ sessionName = name;
439
+ process.stdout.write(`${c.green}✓ Session loaded: ${name} (${messages.length} messages)${c.r}\n`);
440
+ }
240
441
 
442
+ function listSessions() {
443
+ const files = fs.readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.json'));
444
+ if (!files.length) { process.stdout.write(`${c.dim}No saved sessions.${c.r}\n`); return; }
445
+ process.stdout.write(`${c.bold}Saved sessions:${c.r}\n`);
446
+ for (const f of files) {
241
447
  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}✦ ShiroAI:${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
- }
448
+ const s = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf8'));
449
+ const date = new Date(s.timestamp).toLocaleDateString();
450
+ process.stdout.write(` ${c.cyan}${s.name}${c.r} ${c.dim}(${date}, ${s.messages.length} msgs, agent: ${s.agent})${c.r}\n`);
451
+ } catch {}
452
+ }
271
453
  }
272
454
 
273
- // ===== MAIN LOOP =====
274
- header();
455
+ // ===== CONTEXT MANAGEMENT =====
456
+ function addContext(filePath) {
457
+ const fp = path.resolve(CWD, filePath);
458
+ if (!fs.existsSync(fp)) { process.stdout.write(`${c.red}File not found: ${filePath}${c.r}\n`); return; }
459
+ const content = fs.readFileSync(fp, 'utf8');
460
+ contextFiles.push(filePath);
461
+ messages.push({ role: 'system', content: `[Context: ${filePath}]\n${content}` });
462
+ process.stdout.write(`${c.dim}+ Added to context: ${filePath} (${content.length} chars)${c.r}\n`);
463
+ }
275
464
 
276
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
465
+ function showContext() {
466
+ const totalTokens = messages.reduce((sum, m) => sum + Math.ceil((m.content || '').length / 4), 0);
467
+ process.stdout.write(`${c.bold}Context:${c.r}\n`);
468
+ process.stdout.write(` ${c.dim}Messages: ${messages.length}${c.r}\n`);
469
+ process.stdout.write(` ${c.dim}~Tokens: ${totalTokens}${c.r}\n`);
470
+ process.stdout.write(` ${c.dim}Agent: ${currentAgent.name}${c.r}\n`);
471
+ if (contextFiles.length) {
472
+ process.stdout.write(` ${c.dim}Files:${c.r}\n`);
473
+ contextFiles.forEach(f => process.stdout.write(` ${c.cyan}${f}${c.r}\n`));
474
+ }
475
+ }
277
476
 
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
- }
477
+ // ===== UI =====
478
+ function header() {
479
+ console.log(`\n${c.bold}${c.green} ShiroAI${c.r} ${c.dim}v2.0.0${c.r}`);
480
+ console.log(`${c.dim} ${CWD}${c.r}`);
481
+ console.log(`${c.dim} Agent: ${currentAgent.name} | Model: ${MODEL}${c.r}`);
482
+ console.log(`${c.dim} /help for commands${c.r}\n`);
483
+ }
294
484
 
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
- }
485
+ function showHelp() {
486
+ console.log(`${c.bold}Commands:${c.r}`);
487
+ console.log(` ${c.cyan}/help${c.r} Show this help`);
488
+ console.log(` ${c.cyan}/context${c.r} Show context info`);
489
+ console.log(` ${c.cyan}/context add <f>${c.r} Add file to context`);
490
+ console.log(` ${c.cyan}/chat save <name>${c.r} Save session`);
491
+ console.log(` ${c.cyan}/chat load <name>${c.r} Load session`);
492
+ console.log(` ${c.cyan}/chat list${c.r} List sessions`);
493
+ console.log(` ${c.cyan}/agent${c.r} Show current agent`);
494
+ console.log(` ${c.cyan}/agent <name>${c.r} Switch agent`);
495
+ console.log(` ${c.cyan}/agent list${c.r} List agents`);
496
+ console.log(` ${c.cyan}/plan${c.r} Switch to planning mode`);
497
+ console.log(` ${c.cyan}/clear${c.r} Clear conversation`);
498
+ console.log(` ${c.cyan}/quit${c.r} Exit`);
499
+ console.log();
500
+ }
302
501
 
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
- }
502
+ // ===== MAIN =====
503
+ header();
312
504
 
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
- }
505
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
321
506
 
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
- }
507
+ function prompt() {
508
+ const agentTag = currentAgent.name !== 'default' ? `${c.dim}[${currentAgent.name}]${c.r} ` : '';
509
+ rl.question(`${agentTag}${c.cyan}❯ ${c.r}`, async (input) => {
510
+ input = input.trim();
511
+ if (!input) return prompt();
512
+
513
+ // Commands
514
+ if (input === '/quit' || input === '/q' || input === '/exit') { console.log('👋'); process.exit(0); }
515
+ if (input === '/help' || input === '/?') { showHelp(); return prompt(); }
516
+ if (input === '/clear') { initMessages(); contextFiles = []; console.log(`${c.dim}Cleared.${c.r}`); return prompt(); }
517
+ if (input === '/context') { showContext(); return prompt(); }
518
+ if (input.startsWith('/context add ')) { addContext(input.slice(13)); return prompt(); }
519
+
520
+ // Session commands
521
+ if (input.startsWith('/chat save ')) { saveSession(input.slice(11).trim()); return prompt(); }
522
+ if (input.startsWith('/chat load ')) { loadSession(input.slice(11).trim()); return prompt(); }
523
+ if (input === '/chat list') { listSessions(); return prompt(); }
524
+
525
+ // Agent commands
526
+ if (input === '/agent') { console.log(`${c.dim}Current: ${currentAgent.name} — ${currentAgent.description || ''}${c.r}`); return prompt(); }
527
+ if (input === '/agent list') {
528
+ console.log(`${c.bold}Agents:${c.r}`);
529
+ Object.values(AGENTS).forEach(a => console.log(` ${c.cyan}${a.name}${c.r} ${c.dim}— ${a.description || ''}${c.r}`));
530
+ return prompt();
531
+ }
532
+ if (input.startsWith('/agent ')) {
533
+ const name = input.slice(7).trim();
534
+ if (AGENTS[name]) {
535
+ currentAgent = AGENTS[name];
536
+ initMessages();
537
+ console.log(`${c.green}✓ Switched to agent: ${name}${c.r}`);
538
+ } else { console.log(`${c.red}Agent not found: ${name}${c.r}`); }
539
+ return prompt();
540
+ }
330
541
 
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
- }
542
+ // Plan mode shortcut
543
+ if (input === '/plan') {
544
+ currentAgent = AGENTS.plan;
545
+ initMessages();
546
+ console.log(`${c.green}✓ Planning mode. I'll analyze before acting.${c.r}`);
547
+ console.log(`${c.dim} Use /agent default to switch back.${c.r}`);
548
+ return prompt();
549
+ }
338
550
 
339
- // Regular chat
340
- await chat(input);
341
- prompt();
342
- });
551
+ // Regular chat with tool use
552
+ await chat(input);
553
+ prompt();
554
+ });
343
555
  }
344
556
 
345
557
  prompt();
package/package.json CHANGED
@@ -1,22 +1,18 @@
1
1
  {
2
2
  "name": "shiroai",
3
- "version": "1.0.0",
4
- "description": "AI coding agent for your terminal — create, edit, debug, and deploy from the command line",
3
+ "version": "2.0.1",
4
+ "description": "AI coding agent for your terminal — reads, writes, edits files, runs commands",
5
5
  "bin": {
6
6
  "shiroai": "./cli.js"
7
7
  },
8
8
  "files": [
9
9
  "cli.js"
10
10
  ],
11
- "keywords": ["ai", "cli", "coding", "agent", "terminal", "gpt", "copilot", "developer-tools"],
12
- "author": "LeonAI",
11
+ "keywords": ["ai", "cli", "coding", "agent", "terminal", "copilot", "developer-tools", "shiroai"],
12
+ "author": "ShiroAI",
13
13
  "license": "MIT",
14
+ "homepage": "https://shiroai.dev",
14
15
  "engines": {
15
16
  "node": ">=16.0.0"
16
- },
17
- "repository": {
18
- "type": "git",
19
- "url": "https://github.com/leonai/cli"
20
- },
21
- "homepage": "https://leonai.dev"
17
+ }
22
18
  }