shiroai 1.0.0 → 2.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 +9 -9
- package/cli.js +497 -285
- package/package.json +15 -5
package/README.md
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
# ✦
|
|
1
|
+
# ✦ ShiroAI CLI
|
|
2
2
|
|
|
3
3
|
AI coding agent for your terminal. Create, edit, debug, and deploy — all from the command line.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install -g
|
|
8
|
+
npm install -g shiroai
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Setup
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
14
|
+
shiroai login YOUR_API_KEY
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Get your API key at [
|
|
17
|
+
Get your API key at [shiroai.dev](https://shiroai.dev)
|
|
18
18
|
|
|
19
19
|
## Usage
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
|
|
22
|
+
shiroai
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
### Commands
|
|
26
26
|
|
|
27
27
|
| Command | Description |
|
|
28
28
|
|---------|-------------|
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
29
|
+
| `shiroai` | Start interactive session |
|
|
30
|
+
| `shiroai login <key>` | Save API key |
|
|
31
|
+
| `shiroai logout` | Remove API key |
|
|
32
|
+
| `shiroai config` | Show config |
|
|
33
33
|
|
|
34
34
|
### In-session commands
|
|
35
35
|
|
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,
|
|
7
|
+
const { execSync, spawn } = require('child_process');
|
|
8
|
+
const os = require('os');
|
|
7
9
|
|
|
8
10
|
// ===== COLORS =====
|
|
9
11
|
const c = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
//
|
|
36
|
+
// ===== CLI ARGS =====
|
|
31
37
|
const args = process.argv.slice(2);
|
|
32
38
|
if (args[0] === 'login') {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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.
|
|
49
|
+
const API_KEY = config.api_key || process.env.SHIROAI_KEY || '';
|
|
55
50
|
if (!API_KEY) {
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
// =====
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
365
|
+
|
|
366
|
+
req.on('error', reject);
|
|
367
|
+
req.write(body);
|
|
368
|
+
req.end();
|
|
369
|
+
});
|
|
151
370
|
}
|
|
152
371
|
|
|
153
|
-
// =====
|
|
154
|
-
function
|
|
155
|
-
|
|
372
|
+
// ===== CHAT LOOP WITH TOOL USE =====
|
|
373
|
+
async function chat(input) {
|
|
374
|
+
messages.push({ role: 'user', content: input });
|
|
156
375
|
|
|
157
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
207
|
-
return actions;
|
|
421
|
+
process.stdout.write('\n');
|
|
208
422
|
}
|
|
209
423
|
|
|
210
|
-
// =====
|
|
211
|
-
function
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
// =====
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shiroai",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "AI coding agent for your terminal — create, edit, debug, and deploy from the command line",
|
|
5
5
|
"bin": {
|
|
6
6
|
"shiroai": "./cli.js"
|
|
@@ -8,15 +8,25 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
"cli.js"
|
|
10
10
|
],
|
|
11
|
-
"keywords": [
|
|
12
|
-
|
|
11
|
+
"keywords": [
|
|
12
|
+
"ai",
|
|
13
|
+
"cli",
|
|
14
|
+
"coding",
|
|
15
|
+
"agent",
|
|
16
|
+
"terminal",
|
|
17
|
+
"gpt",
|
|
18
|
+
"copilot",
|
|
19
|
+
"developer-tools",
|
|
20
|
+
"shiroai"
|
|
21
|
+
],
|
|
22
|
+
"author": "ShiroAI",
|
|
13
23
|
"license": "MIT",
|
|
14
24
|
"engines": {
|
|
15
25
|
"node": ">=16.0.0"
|
|
16
26
|
},
|
|
17
27
|
"repository": {
|
|
18
28
|
"type": "git",
|
|
19
|
-
"url": "https://github.com/
|
|
29
|
+
"url": "https://github.com/shiroai/cli"
|
|
20
30
|
},
|
|
21
|
-
"homepage": "https://
|
|
31
|
+
"homepage": "https://shiroai.dev"
|
|
22
32
|
}
|