otoro-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/otoro.js +93 -0
- package/lib/agent.js +250 -0
- package/lib/api.js +64 -0
- package/lib/chat.js +217 -0
- package/lib/config.js +54 -0
- package/lib/image.js +50 -0
- package/lib/task.js +61 -0
- package/lib/tools.js +105 -0
- package/package.json +23 -0
package/bin/otoro.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { program } = require('commander')
|
|
3
|
+
const { startChat } = require('../lib/chat')
|
|
4
|
+
const { runTask } = require('../lib/task')
|
|
5
|
+
const { initConfig, getConfig } = require('../lib/config')
|
|
6
|
+
const chalk = require('chalk')
|
|
7
|
+
const pkg = require('../package.json')
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name('otoro')
|
|
11
|
+
.description('Otoro AGI — AI coding assistant')
|
|
12
|
+
.version(pkg.version)
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.command('init')
|
|
16
|
+
.description('Initialize Otoro in this project')
|
|
17
|
+
.action(async () => {
|
|
18
|
+
await initConfig()
|
|
19
|
+
console.log(chalk.green('✓ Otoro initialized. Run `otoro` to start coding.'))
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.command('chat')
|
|
24
|
+
.alias('c')
|
|
25
|
+
.description('Start interactive chat')
|
|
26
|
+
.action(async () => {
|
|
27
|
+
await startChat()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command('ask <prompt...>')
|
|
32
|
+
.description('Ask Otoro a single question')
|
|
33
|
+
.action(async (prompt) => {
|
|
34
|
+
const { askOnce } = require('../lib/chat')
|
|
35
|
+
await askOnce(prompt.join(' '))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
program
|
|
39
|
+
.command('run <task...>')
|
|
40
|
+
.description('Run a coding task (write + execute + verify)')
|
|
41
|
+
.action(async (task) => {
|
|
42
|
+
await runTask(task.join(' '))
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
program
|
|
46
|
+
.command('image <prompt...>')
|
|
47
|
+
.description('Generate an image')
|
|
48
|
+
.action(async (prompt) => {
|
|
49
|
+
const { generateImage } = require('../lib/image')
|
|
50
|
+
await generateImage(prompt.join(' '))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
program
|
|
54
|
+
.command('start')
|
|
55
|
+
.description('Start Otoro agent daemon — connects to server, waits for remote tasks')
|
|
56
|
+
.action(async () => {
|
|
57
|
+
const { startAgent } = require('../lib/agent')
|
|
58
|
+
await startAgent()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
program
|
|
62
|
+
.command('status')
|
|
63
|
+
.description('Check connected agents and server status')
|
|
64
|
+
.action(async () => {
|
|
65
|
+
const http = require('http')
|
|
66
|
+
const config = getConfig()
|
|
67
|
+
const url = `${config.gpu_url}/v1/agents`
|
|
68
|
+
http.get(url, { headers: { 'Authorization': `Bearer ${config.api_key}` } }, (res) => {
|
|
69
|
+
let data = ''
|
|
70
|
+
res.on('data', c => data += c)
|
|
71
|
+
res.on('end', () => {
|
|
72
|
+
try {
|
|
73
|
+
const d = JSON.parse(data)
|
|
74
|
+
console.log(chalk.cyan.bold('\n 🐙 Connected Agents:\n'))
|
|
75
|
+
if (d.agents?.length === 0) {
|
|
76
|
+
console.log(chalk.gray(' No agents connected. Run `otoro start` on your machine.\n'))
|
|
77
|
+
} else {
|
|
78
|
+
for (const a of d.agents) {
|
|
79
|
+
console.log(chalk.green(` ● ${a.hostname}`) + chalk.gray(` — ${a.cwd} (${a.platform})`))
|
|
80
|
+
}
|
|
81
|
+
console.log()
|
|
82
|
+
}
|
|
83
|
+
} catch { console.log(chalk.red(' Failed to fetch status')) }
|
|
84
|
+
})
|
|
85
|
+
}).on('error', (e) => console.log(chalk.red(` Error: ${e.message}`)))
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Default: start interactive chat
|
|
89
|
+
if (process.argv.length <= 2) {
|
|
90
|
+
startChat()
|
|
91
|
+
} else {
|
|
92
|
+
program.parse()
|
|
93
|
+
}
|
package/lib/agent.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
const WebSocket = require('ws')
|
|
2
|
+
const os = require('os')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const chalk = require('chalk')
|
|
5
|
+
const { getConfig } = require('./config')
|
|
6
|
+
const { readFile, writeFile, editFile, listFiles, runCommand, searchCode } = require('./tools')
|
|
7
|
+
const { chatCompletion } = require('./api')
|
|
8
|
+
|
|
9
|
+
class OtoroAgent {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.config = getConfig()
|
|
12
|
+
this.agentId = `${os.hostname()}-${process.pid}`
|
|
13
|
+
this.ws = null
|
|
14
|
+
this.reconnectDelay = 3000
|
|
15
|
+
this.running = false
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async start() {
|
|
19
|
+
this.running = true
|
|
20
|
+
console.log(chalk.cyan.bold('\n 🐙 Otoro Agent — Daemon Mode\n'))
|
|
21
|
+
console.log(chalk.gray(` Agent ID: ${this.agentId}`))
|
|
22
|
+
console.log(chalk.gray(` Hostname: ${os.hostname()}`))
|
|
23
|
+
console.log(chalk.gray(` CWD: ${process.cwd()}`))
|
|
24
|
+
console.log(chalk.gray(` Server: ${this.config.gpu_url}\n`))
|
|
25
|
+
console.log(chalk.green(' Agent running. You can now send tasks from otoroagi.com or your phone.'))
|
|
26
|
+
console.log(chalk.gray(' Press Ctrl+C to stop.\n'))
|
|
27
|
+
|
|
28
|
+
this.connect()
|
|
29
|
+
|
|
30
|
+
// Keep process alive
|
|
31
|
+
process.on('SIGINT', () => {
|
|
32
|
+
console.log(chalk.yellow('\n Shutting down agent...'))
|
|
33
|
+
this.running = false
|
|
34
|
+
if (this.ws) this.ws.close()
|
|
35
|
+
process.exit(0)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
connect() {
|
|
40
|
+
const wsUrl = this.config.gpu_url.replace('http://', 'ws://').replace('https://', 'wss://') + '/v1/agent/ws'
|
|
41
|
+
console.log(chalk.gray(` Connecting to ${wsUrl}...`))
|
|
42
|
+
|
|
43
|
+
this.ws = new WebSocket(wsUrl)
|
|
44
|
+
|
|
45
|
+
this.ws.on('open', () => {
|
|
46
|
+
console.log(chalk.green(' ✓ Connected to Otoro server'))
|
|
47
|
+
// Send auth + agent info
|
|
48
|
+
this.ws.send(JSON.stringify({
|
|
49
|
+
api_key: this.config.api_key,
|
|
50
|
+
agent_id: this.agentId,
|
|
51
|
+
user_id: this.config.user_id || '',
|
|
52
|
+
hostname: os.hostname(),
|
|
53
|
+
cwd: process.cwd(),
|
|
54
|
+
platform: `${os.platform()} ${os.arch()}`,
|
|
55
|
+
project: path.basename(process.cwd()),
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
// Start heartbeat
|
|
59
|
+
this.heartbeatInterval = setInterval(() => {
|
|
60
|
+
if (this.ws.readyState === WebSocket.OPEN) {
|
|
61
|
+
this.ws.send(JSON.stringify({ type: 'heartbeat' }))
|
|
62
|
+
}
|
|
63
|
+
}, 30000)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
this.ws.on('message', async (data) => {
|
|
67
|
+
try {
|
|
68
|
+
const msg = JSON.parse(data.toString())
|
|
69
|
+
if (msg.type === 'task') {
|
|
70
|
+
await this.handleTask(msg)
|
|
71
|
+
} else if (msg.type === 'pong') {
|
|
72
|
+
// heartbeat response
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error(chalk.red(` Error processing message: ${e.message}`))
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
this.ws.on('close', () => {
|
|
80
|
+
console.log(chalk.yellow(' Disconnected from server'))
|
|
81
|
+
clearInterval(this.heartbeatInterval)
|
|
82
|
+
if (this.running) {
|
|
83
|
+
console.log(chalk.gray(` Reconnecting in ${this.reconnectDelay / 1000}s...`))
|
|
84
|
+
setTimeout(() => this.connect(), this.reconnectDelay)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
this.ws.on('error', (err) => {
|
|
89
|
+
console.log(chalk.red(` Connection error: ${err.message}`))
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async handleTask(task) {
|
|
94
|
+
const { task_id, task_type, payload } = task
|
|
95
|
+
console.log(chalk.cyan(`\n 📥 Task received: [${task_id}] ${task_type}`))
|
|
96
|
+
|
|
97
|
+
let result = {}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
switch (task_type) {
|
|
101
|
+
case 'chat':
|
|
102
|
+
case 'code':
|
|
103
|
+
result = await this.handleChatTask(payload)
|
|
104
|
+
break
|
|
105
|
+
case 'read':
|
|
106
|
+
result = readFile(payload.path)
|
|
107
|
+
break
|
|
108
|
+
case 'write':
|
|
109
|
+
result = writeFile(payload.path, payload.content)
|
|
110
|
+
console.log(chalk.green(` ✓ Written: ${payload.path}`))
|
|
111
|
+
break
|
|
112
|
+
case 'edit':
|
|
113
|
+
result = editFile(payload.path, payload.old, payload.new)
|
|
114
|
+
console.log(chalk.green(` ✓ Edited: ${payload.path}`))
|
|
115
|
+
break
|
|
116
|
+
case 'run_command':
|
|
117
|
+
console.log(chalk.yellow(` ⚡ Running: ${payload.cmd}`))
|
|
118
|
+
result = runCommand(payload.cmd, payload.timeout || 30000)
|
|
119
|
+
break
|
|
120
|
+
case 'list_files':
|
|
121
|
+
result = listFiles(payload.dir || '.', payload.pattern || '')
|
|
122
|
+
break
|
|
123
|
+
case 'search':
|
|
124
|
+
result = searchCode(payload.pattern, payload.dir || '.')
|
|
125
|
+
break
|
|
126
|
+
case 'full_task':
|
|
127
|
+
result = await this.handleFullTask(payload)
|
|
128
|
+
break
|
|
129
|
+
default:
|
|
130
|
+
result = { error: `Unknown task type: ${task_type}` }
|
|
131
|
+
}
|
|
132
|
+
} catch (e) {
|
|
133
|
+
result = { error: e.message }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Send result back to server
|
|
137
|
+
this.ws.send(JSON.stringify({
|
|
138
|
+
type: 'task_result',
|
|
139
|
+
result: { task_id, ...result },
|
|
140
|
+
}))
|
|
141
|
+
console.log(chalk.green(` ✓ Task [${task_id}] completed`))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async handleChatTask(payload) {
|
|
145
|
+
const { message, context } = payload
|
|
146
|
+
|
|
147
|
+
// Gather project context
|
|
148
|
+
const files = listFiles('.', '')
|
|
149
|
+
const projectContext = `Project: ${path.basename(process.cwd())}
|
|
150
|
+
Working directory: ${process.cwd()}
|
|
151
|
+
Files (${files.total}): ${files.files?.slice(0, 30).join(', ')}`
|
|
152
|
+
|
|
153
|
+
const messages = [
|
|
154
|
+
{ role: 'system', content: `You are Otoro running as a local agent on the user's machine.
|
|
155
|
+
You have FULL access to their project files. Working directory: ${process.cwd()}
|
|
156
|
+
|
|
157
|
+
Available tools (use XML tags in your response):
|
|
158
|
+
- <tool:read path="file"/> — read a file
|
|
159
|
+
- <tool:write path="file">content</tool:write> — write/create a file
|
|
160
|
+
- <tool:edit path="file" old="old text" new="new text"/> — edit a file
|
|
161
|
+
- <tool:run cmd="command"/> — run a shell command
|
|
162
|
+
- <tool:search pattern="regex"/> — search code
|
|
163
|
+
|
|
164
|
+
Project context:
|
|
165
|
+
${projectContext}
|
|
166
|
+
|
|
167
|
+
Be direct. Execute tasks immediately. Don't ask for permission — just do it.` },
|
|
168
|
+
...(context || []),
|
|
169
|
+
{ role: 'user', content: message },
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
const result = await chatCompletion(messages, { max_tokens: 8192 })
|
|
173
|
+
const response = result.choices?.[0]?.message?.content || ''
|
|
174
|
+
|
|
175
|
+
// Process tool calls in the response
|
|
176
|
+
const toolResults = await this.processTools(response)
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
response,
|
|
180
|
+
tool_results: toolResults,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async handleFullTask(payload) {
|
|
185
|
+
// Multi-step AGI task — AI plans, executes, verifies
|
|
186
|
+
const { task, max_iterations = 5 } = payload
|
|
187
|
+
const steps = []
|
|
188
|
+
let context = []
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < max_iterations; i++) {
|
|
191
|
+
const stepMsg = i === 0 ? task : 'Continue with the next step. If done, say "TASK COMPLETE".'
|
|
192
|
+
const result = await this.handleChatTask({ message: stepMsg, context })
|
|
193
|
+
|
|
194
|
+
steps.push({ step: i + 1, response: result.response, tools: result.tool_results })
|
|
195
|
+
context.push({ role: 'user', content: stepMsg })
|
|
196
|
+
context.push({ role: 'assistant', content: result.response })
|
|
197
|
+
|
|
198
|
+
if (result.response.includes('TASK COMPLETE')) break
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { steps, total_steps: steps.length }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async processTools(response) {
|
|
205
|
+
const results = []
|
|
206
|
+
|
|
207
|
+
// Process <tool:read>
|
|
208
|
+
for (const match of response.matchAll(/<tool:read\s+path="([^"]+)"\s*\/>/g)) {
|
|
209
|
+
const r = readFile(match[1])
|
|
210
|
+
results.push({ type: 'read', path: match[1], ...r })
|
|
211
|
+
if (r.success) console.log(chalk.gray(` 📄 Read: ${match[1]}`))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Process <tool:write>
|
|
215
|
+
for (const match of response.matchAll(/<tool:write\s+path="([^"]+)">([\s\S]*?)<\/tool:write>/g)) {
|
|
216
|
+
const r = writeFile(match[1], match[2].trim())
|
|
217
|
+
results.push({ type: 'write', path: match[1], ...r })
|
|
218
|
+
if (r.success) console.log(chalk.green(` ✏️ Written: ${match[1]}`))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Process <tool:edit>
|
|
222
|
+
for (const match of response.matchAll(/<tool:edit\s+path="([^"]+)"\s+old="([^"]*?)"\s+new="([^"]*?)"\s*\/>/g)) {
|
|
223
|
+
const r = editFile(match[1], match[2], match[3])
|
|
224
|
+
results.push({ type: 'edit', path: match[1], ...r })
|
|
225
|
+
if (r.success) console.log(chalk.green(` ✏️ Edited: ${match[1]}`))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Process <tool:run>
|
|
229
|
+
for (const match of response.matchAll(/<tool:run\s+cmd="([^"]+)"\s*\/>/g)) {
|
|
230
|
+
console.log(chalk.yellow(` ⚡ ${match[1]}`))
|
|
231
|
+
const r = runCommand(match[1])
|
|
232
|
+
results.push({ type: 'run', cmd: match[1], ...r })
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Process <tool:search>
|
|
236
|
+
for (const match of response.matchAll(/<tool:search\s+pattern="([^"]+)"\s*\/>/g)) {
|
|
237
|
+
const r = searchCode(match[1])
|
|
238
|
+
results.push({ type: 'search', pattern: match[1], ...r })
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return results
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function startAgent() {
|
|
246
|
+
const agent = new OtoroAgent()
|
|
247
|
+
await agent.start()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = { startAgent }
|
package/lib/api.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const https = require('https')
|
|
2
|
+
const http = require('http')
|
|
3
|
+
const { getConfig } = require('./config')
|
|
4
|
+
|
|
5
|
+
async function chatCompletion(messages, options = {}) {
|
|
6
|
+
const config = getConfig()
|
|
7
|
+
const url = new URL(`${config.gpu_url}/v1/chat/completions`)
|
|
8
|
+
const body = JSON.stringify({
|
|
9
|
+
model: options.model || config.model || 'qwen-coder',
|
|
10
|
+
messages,
|
|
11
|
+
max_tokens: options.max_tokens || 8192,
|
|
12
|
+
temperature: options.temperature || 0.3,
|
|
13
|
+
stream: false,
|
|
14
|
+
})
|
|
15
|
+
return makeRequest(url, body, config.api_key)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function executeCode(code, language = 'python') {
|
|
19
|
+
const config = getConfig()
|
|
20
|
+
const url = new URL(`${config.gpu_url}/v1/agent/execute`)
|
|
21
|
+
const body = JSON.stringify({ code, language })
|
|
22
|
+
return makeRequest(url, body, config.api_key)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function searchWeb(query) {
|
|
26
|
+
const config = getConfig()
|
|
27
|
+
const url = new URL(`${config.gpu_url}/v1/agent/search`)
|
|
28
|
+
const body = JSON.stringify({ query, max_results: 5 })
|
|
29
|
+
return makeRequest(url, body, config.api_key)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function cveCheck(code, language = '') {
|
|
33
|
+
const config = getConfig()
|
|
34
|
+
const url = new URL(`${config.gpu_url}/v1/security/cve-check`)
|
|
35
|
+
const body = JSON.stringify({ code, language })
|
|
36
|
+
return makeRequest(url, body, config.api_key)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeRequest(url, body, apiKey) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const mod = url.protocol === 'https:' ? https : http
|
|
42
|
+
const req = mod.request(url, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
47
|
+
},
|
|
48
|
+
timeout: 180000,
|
|
49
|
+
}, (res) => {
|
|
50
|
+
let data = ''
|
|
51
|
+
res.on('data', chunk => data += chunk)
|
|
52
|
+
res.on('end', () => {
|
|
53
|
+
try { resolve(JSON.parse(data)) }
|
|
54
|
+
catch { reject(new Error(`Non-JSON response: ${data.slice(0, 200)}`)) }
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
req.on('error', reject)
|
|
58
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')) })
|
|
59
|
+
req.write(body)
|
|
60
|
+
req.end()
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { chatCompletion, executeCode, searchWeb, cveCheck }
|
package/lib/chat.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
const readline = require('readline')
|
|
2
|
+
const chalk = require('chalk')
|
|
3
|
+
const ora = require('ora')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
const { chatCompletion, executeCode, cveCheck } = require('./api')
|
|
7
|
+
const { readFile, writeFile, editFile, listFiles, runCommand, searchCode } = require('./tools')
|
|
8
|
+
|
|
9
|
+
const SYSTEM_PROMPT = `You are Otoro — an expert AI coding assistant running in the user's terminal.
|
|
10
|
+
|
|
11
|
+
You have access to the user's project files. The current working directory is: ${process.cwd()}
|
|
12
|
+
|
|
13
|
+
TOOLS — You can use these by including special tags in your response:
|
|
14
|
+
|
|
15
|
+
1. READ FILE: <tool:read path="filename"/>
|
|
16
|
+
2. WRITE FILE: <tool:write path="filename">content here</tool:write>
|
|
17
|
+
3. EDIT FILE: <tool:edit path="filename" old="old text" new="new text"/>
|
|
18
|
+
4. RUN COMMAND: <tool:run cmd="command here"/>
|
|
19
|
+
5. SEARCH CODE: <tool:search pattern="regex" dir="."/>
|
|
20
|
+
6. LIST FILES: <tool:ls dir="." pattern=""/>
|
|
21
|
+
|
|
22
|
+
RULES:
|
|
23
|
+
- Always read files before editing them
|
|
24
|
+
- Show the user what you're doing
|
|
25
|
+
- Write clean, working code
|
|
26
|
+
- Be direct and concise — no fluff
|
|
27
|
+
- When writing files, use <tool:write> tags
|
|
28
|
+
- When you need to see a file, use <tool:read>
|
|
29
|
+
- NEVER guess file contents — always read first
|
|
30
|
+
- Speak English only`
|
|
31
|
+
|
|
32
|
+
const history = []
|
|
33
|
+
|
|
34
|
+
async function startChat() {
|
|
35
|
+
console.log(chalk.cyan.bold('\n 🐙 Otoro AGI'))
|
|
36
|
+
console.log(chalk.gray(` Working in: ${process.cwd()}`))
|
|
37
|
+
|
|
38
|
+
// Auto-detect project
|
|
39
|
+
const files = listFiles('.', '')
|
|
40
|
+
const projectFiles = files.files ? files.files.slice(0, 30) : []
|
|
41
|
+
console.log(chalk.gray(` Found ${files.total || 0} files\n`))
|
|
42
|
+
console.log(chalk.gray(' Type your request. Use /help for commands. Ctrl+C to exit.\n'))
|
|
43
|
+
|
|
44
|
+
const rl = readline.createInterface({
|
|
45
|
+
input: process.stdin,
|
|
46
|
+
output: process.stdout,
|
|
47
|
+
prompt: chalk.green(' ❯ '),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
rl.prompt()
|
|
51
|
+
|
|
52
|
+
rl.on('line', async (input) => {
|
|
53
|
+
const line = input.trim()
|
|
54
|
+
if (!line) { rl.prompt(); return }
|
|
55
|
+
|
|
56
|
+
if (line === '/help') {
|
|
57
|
+
console.log(chalk.cyan(`
|
|
58
|
+
Commands:
|
|
59
|
+
/help Show this help
|
|
60
|
+
/files List project files
|
|
61
|
+
/read <file> Read a file
|
|
62
|
+
/run <cmd> Run a shell command
|
|
63
|
+
/clear Clear conversation
|
|
64
|
+
/exit Exit Otoro
|
|
65
|
+
`))
|
|
66
|
+
rl.prompt(); return
|
|
67
|
+
}
|
|
68
|
+
if (line === '/exit' || line === '/quit') { process.exit(0) }
|
|
69
|
+
if (line === '/clear') { history.length = 0; console.log(chalk.gray(' Conversation cleared.')); rl.prompt(); return }
|
|
70
|
+
if (line === '/files') {
|
|
71
|
+
const r = listFiles('.')
|
|
72
|
+
console.log(chalk.gray(`\n ${r.files.slice(0, 40).join('\n ')}\n ... ${r.total} total\n`))
|
|
73
|
+
rl.prompt(); return
|
|
74
|
+
}
|
|
75
|
+
if (line.startsWith('/read ')) {
|
|
76
|
+
const f = readFile(line.slice(6).trim())
|
|
77
|
+
if (f.success) console.log(chalk.gray(`\n${f.content}\n`))
|
|
78
|
+
else console.log(chalk.red(` Error: ${f.error}`))
|
|
79
|
+
rl.prompt(); return
|
|
80
|
+
}
|
|
81
|
+
if (line.startsWith('/run ')) {
|
|
82
|
+
const r = runCommand(line.slice(5).trim())
|
|
83
|
+
console.log(r.success ? chalk.gray(`\n${r.output}`) : chalk.red(`\n${r.error}`))
|
|
84
|
+
rl.prompt(); return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Include file context if user mentions a file
|
|
88
|
+
let contextMsg = line
|
|
89
|
+
const fileRefs = line.match(/[\w./\-]+\.\w{1,5}/g) || []
|
|
90
|
+
for (const ref of fileRefs.slice(0, 3)) {
|
|
91
|
+
if (fs.existsSync(ref)) {
|
|
92
|
+
const content = readFile(ref)
|
|
93
|
+
if (content.success && content.content.length < 10000) {
|
|
94
|
+
contextMsg += `\n\n[File: ${ref}]\n\`\`\`\n${content.content}\n\`\`\``
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
history.push({ role: 'user', content: contextMsg })
|
|
100
|
+
|
|
101
|
+
const spinner = ora({ text: chalk.gray('Thinking...'), color: 'cyan' }).start()
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const messages = [{ role: 'system', content: SYSTEM_PROMPT }, ...history.slice(-20)]
|
|
105
|
+
const result = await chatCompletion(messages)
|
|
106
|
+
const response = result.choices?.[0]?.message?.content || 'No response'
|
|
107
|
+
spinner.stop()
|
|
108
|
+
|
|
109
|
+
history.push({ role: 'assistant', content: response })
|
|
110
|
+
|
|
111
|
+
// Process tool calls in response
|
|
112
|
+
await processResponse(response)
|
|
113
|
+
|
|
114
|
+
} catch (e) {
|
|
115
|
+
spinner.fail(chalk.red(`Error: ${e.message}`))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
rl.prompt()
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function processResponse(response) {
|
|
123
|
+
// Handle tool calls embedded in response
|
|
124
|
+
let display = response
|
|
125
|
+
|
|
126
|
+
// Process <tool:read> tags
|
|
127
|
+
const reads = response.matchAll(/<tool:read\s+path="([^"]+)"\s*\/>/g)
|
|
128
|
+
for (const match of reads) {
|
|
129
|
+
const result = readFile(match[1])
|
|
130
|
+
if (result.success) {
|
|
131
|
+
console.log(chalk.gray(` 📄 Read: ${match[1]} (${result.lines} lines)`))
|
|
132
|
+
history.push({ role: 'user', content: `[File ${match[1]}]:\n\`\`\`\n${result.content.slice(0, 8000)}\n\`\`\`` })
|
|
133
|
+
}
|
|
134
|
+
display = display.replace(match[0], '')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Process <tool:write> tags
|
|
138
|
+
const writes = response.matchAll(/<tool:write\s+path="([^"]+)">([\s\S]*?)<\/tool:write>/g)
|
|
139
|
+
for (const match of writes) {
|
|
140
|
+
const [, filePath, content] = match
|
|
141
|
+
console.log(chalk.yellow(` ✏️ Write: ${filePath}`))
|
|
142
|
+
const result = writeFile(filePath, content.trim())
|
|
143
|
+
if (result.success) console.log(chalk.green(` ✓ Written ${result.lines} lines to ${filePath}`))
|
|
144
|
+
else console.log(chalk.red(` ✗ ${result.error}`))
|
|
145
|
+
display = display.replace(match[0], `[Written to ${filePath}]`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Process <tool:edit> tags
|
|
149
|
+
const edits = response.matchAll(/<tool:edit\s+path="([^"]+)"\s+old="([^"]*?)"\s+new="([^"]*?)"\s*\/>/g)
|
|
150
|
+
for (const match of edits) {
|
|
151
|
+
const [, filePath, oldStr, newStr] = match
|
|
152
|
+
console.log(chalk.yellow(` ✏️ Edit: ${filePath}`))
|
|
153
|
+
const result = editFile(filePath, oldStr, newStr)
|
|
154
|
+
if (result.success) console.log(chalk.green(` ✓ Edited ${filePath}`))
|
|
155
|
+
else console.log(chalk.red(` ✗ ${result.error}`))
|
|
156
|
+
display = display.replace(match[0], `[Edited ${filePath}]`)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Process <tool:run> tags
|
|
160
|
+
const runs = response.matchAll(/<tool:run\s+cmd="([^"]+)"\s*\/>/g)
|
|
161
|
+
for (const match of runs) {
|
|
162
|
+
console.log(chalk.yellow(` ⚡ Run: ${match[1]}`))
|
|
163
|
+
const result = runCommand(match[1])
|
|
164
|
+
if (result.success) {
|
|
165
|
+
console.log(chalk.gray(` ${result.output.slice(0, 500)}`))
|
|
166
|
+
} else {
|
|
167
|
+
console.log(chalk.red(` ${result.error}`))
|
|
168
|
+
}
|
|
169
|
+
display = display.replace(match[0], '')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Process <tool:search> tags
|
|
173
|
+
const searches = response.matchAll(/<tool:search\s+pattern="([^"]+)"(?:\s+dir="([^"]*)")?\s*\/>/g)
|
|
174
|
+
for (const match of searches) {
|
|
175
|
+
const result = searchCode(match[1], match[2] || '.')
|
|
176
|
+
if (result.results) console.log(chalk.gray(` 🔍 ${result.results.slice(0, 500)}`))
|
|
177
|
+
display = display.replace(match[0], '')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Process <tool:ls> tags
|
|
181
|
+
const lsMatches = response.matchAll(/<tool:ls(?:\s+dir="([^"]*)")?(?:\s+pattern="([^"]*)")?\s*\/>/g)
|
|
182
|
+
for (const match of lsMatches) {
|
|
183
|
+
const result = listFiles(match[1] || '.', match[2] || '')
|
|
184
|
+
console.log(chalk.gray(` 📁 ${result.files?.slice(0, 20).join(', ')}`))
|
|
185
|
+
display = display.replace(match[0], '')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Clean remaining tags and display
|
|
189
|
+
display = display.replace(/<tool:[^>]*>/g, '').replace(/<\/tool:\w+>/g, '').trim()
|
|
190
|
+
if (display) {
|
|
191
|
+
// Format code blocks nicely
|
|
192
|
+
display = display.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
|
|
193
|
+
return chalk.gray(` ┌─ ${lang || 'code'} ─`) + '\n' +
|
|
194
|
+
code.split('\n').map(l => chalk.white(` │ ${l}`)).join('\n') + '\n' +
|
|
195
|
+
chalk.gray(' └───')
|
|
196
|
+
})
|
|
197
|
+
console.log('\n' + display.split('\n').map(l => ` ${l}`).join('\n') + '\n')
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function askOnce(prompt) {
|
|
202
|
+
const spinner = ora({ text: chalk.gray('Thinking...'), color: 'cyan' }).start()
|
|
203
|
+
try {
|
|
204
|
+
const messages = [
|
|
205
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
206
|
+
{ role: 'user', content: prompt },
|
|
207
|
+
]
|
|
208
|
+
const result = await chatCompletion(messages)
|
|
209
|
+
const response = result.choices?.[0]?.message?.content || 'No response'
|
|
210
|
+
spinner.stop()
|
|
211
|
+
await processResponse(response)
|
|
212
|
+
} catch (e) {
|
|
213
|
+
spinner.fail(chalk.red(`Error: ${e.message}`))
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = { startChat, askOnce }
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const os = require('os')
|
|
4
|
+
const chalk = require('chalk')
|
|
5
|
+
const readline = require('readline')
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), '.otoro')
|
|
8
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
|
|
9
|
+
const PROJECT_FILE = '.otoro.json'
|
|
10
|
+
|
|
11
|
+
const DEFAULT_CONFIG = {
|
|
12
|
+
api_url: 'https://otoroagi.com/api/otoro',
|
|
13
|
+
gpu_url: 'http://100.124.135.21:8000',
|
|
14
|
+
api_key: 'otoro-secret',
|
|
15
|
+
model: 'qwen-coder',
|
|
16
|
+
theme: 'dark',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getConfig() {
|
|
20
|
+
try {
|
|
21
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
22
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) }
|
|
23
|
+
}
|
|
24
|
+
} catch {}
|
|
25
|
+
return DEFAULT_CONFIG
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function saveConfig(config) {
|
|
29
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
30
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function initConfig() {
|
|
34
|
+
const config = getConfig()
|
|
35
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
36
|
+
const ask = (q) => new Promise(r => rl.question(q, r))
|
|
37
|
+
|
|
38
|
+
console.log(chalk.cyan.bold('\n 🐙 Otoro AGI Setup\n'))
|
|
39
|
+
|
|
40
|
+
const url = await ask(chalk.gray(` GPU URL [${config.gpu_url}]: `))
|
|
41
|
+
if (url.trim()) config.gpu_url = url.trim()
|
|
42
|
+
|
|
43
|
+
const key = await ask(chalk.gray(` API Key [${config.api_key}]: `))
|
|
44
|
+
if (key.trim()) config.api_key = key.trim()
|
|
45
|
+
|
|
46
|
+
rl.close()
|
|
47
|
+
saveConfig(config)
|
|
48
|
+
|
|
49
|
+
// Create project config
|
|
50
|
+
const projectConfig = { name: path.basename(process.cwd()), created: new Date().toISOString() }
|
|
51
|
+
fs.writeFileSync(PROJECT_FILE, JSON.stringify(projectConfig, null, 2))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { getConfig, saveConfig, initConfig }
|
package/lib/image.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const chalk = require('chalk')
|
|
2
|
+
const ora = require('ora')
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const http = require('http')
|
|
5
|
+
const { getConfig } = require('./config')
|
|
6
|
+
|
|
7
|
+
async function generateImage(prompt) {
|
|
8
|
+
const config = getConfig()
|
|
9
|
+
console.log(chalk.cyan.bold('\n 🐙 Otoro AGI — Image Generator\n'))
|
|
10
|
+
console.log(chalk.gray(` Prompt: ${prompt}\n`))
|
|
11
|
+
|
|
12
|
+
const spinner = ora({ text: chalk.gray('Generating image with FLUX.1-dev...'), color: 'cyan' }).start()
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(`${config.gpu_url}/v1/images/generations`)
|
|
16
|
+
const body = JSON.stringify({ prompt, size: '1024x1024', quality: 'standard' })
|
|
17
|
+
|
|
18
|
+
const result = await new Promise((resolve, reject) => {
|
|
19
|
+
const req = http.request(url, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.api_key}` },
|
|
22
|
+
timeout: 120000,
|
|
23
|
+
}, (res) => {
|
|
24
|
+
let data = ''
|
|
25
|
+
res.on('data', c => data += c)
|
|
26
|
+
res.on('end', () => { try { resolve(JSON.parse(data)) } catch { reject(new Error('Bad response')) } })
|
|
27
|
+
})
|
|
28
|
+
req.on('error', reject)
|
|
29
|
+
req.write(body)
|
|
30
|
+
req.end()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const filename = result.data?.[0]?.filename
|
|
34
|
+
const elapsed = result.elapsed_seconds
|
|
35
|
+
spinner.succeed(chalk.green(`Generated in ${elapsed}s`))
|
|
36
|
+
|
|
37
|
+
if (filename) {
|
|
38
|
+
// Save locally
|
|
39
|
+
const localPath = `./${filename}`
|
|
40
|
+
const b64 = result.data[0].b64_json
|
|
41
|
+
fs.writeFileSync(localPath, Buffer.from(b64, 'base64'))
|
|
42
|
+
console.log(chalk.green(` ✓ Saved to ${localPath}`))
|
|
43
|
+
console.log(chalk.gray(` URL: https://otoroagi.com/api/otoro/images/${filename}\n`))
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
spinner.fail(chalk.red(`Error: ${e.message}`))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { generateImage }
|
package/lib/task.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const chalk = require('chalk')
|
|
2
|
+
const ora = require('ora')
|
|
3
|
+
const { chatCompletion, executeCode, cveCheck } = require('./api')
|
|
4
|
+
|
|
5
|
+
async function runTask(task) {
|
|
6
|
+
console.log(chalk.cyan.bold('\n 🐙 Otoro AGI — Task Runner\n'))
|
|
7
|
+
console.log(chalk.gray(` Task: ${task}\n`))
|
|
8
|
+
|
|
9
|
+
const spinner = ora({ text: chalk.gray('Writing code...'), color: 'cyan' }).start()
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const messages = [
|
|
13
|
+
{ role: 'system', content: 'You are Otoro, an expert coding assistant. Write complete, runnable code. Wrap code in ```python or ```javascript code blocks. Include test cases.' },
|
|
14
|
+
{ role: 'user', content: task },
|
|
15
|
+
]
|
|
16
|
+
const result = await chatCompletion(messages, { max_tokens: 4096 })
|
|
17
|
+
const response = result.choices?.[0]?.message?.content || ''
|
|
18
|
+
spinner.succeed(chalk.green('Code generated'))
|
|
19
|
+
|
|
20
|
+
console.log(chalk.gray('\n ' + response.slice(0, 2000).split('\n').join('\n ') + '\n'))
|
|
21
|
+
|
|
22
|
+
// Extract code blocks
|
|
23
|
+
const blocks = [...response.matchAll(/```(\w*)\n([\s\S]*?)```/g)]
|
|
24
|
+
if (blocks.length === 0) { console.log(chalk.yellow(' No code blocks found.')); return }
|
|
25
|
+
|
|
26
|
+
for (const [, lang, code] of blocks) {
|
|
27
|
+
const language = (lang || 'python').toLowerCase()
|
|
28
|
+
if (!['python', 'javascript', 'bash'].includes(language)) continue
|
|
29
|
+
|
|
30
|
+
console.log(chalk.cyan(` ⚡ Executing ${language}...`))
|
|
31
|
+
const execResult = await executeCode(code, language)
|
|
32
|
+
|
|
33
|
+
if (execResult.success) {
|
|
34
|
+
console.log(chalk.green(' ✓ Success'))
|
|
35
|
+
if (execResult.stdout) console.log(chalk.gray(` ${execResult.stdout.split('\n').join('\n ')}`))
|
|
36
|
+
} else {
|
|
37
|
+
console.log(chalk.red(` ✗ Failed (exit ${execResult.exit_code})`))
|
|
38
|
+
if (execResult.stderr) console.log(chalk.red(` ${execResult.stderr.slice(0, 500)}`))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// CVE check
|
|
42
|
+
console.log(chalk.gray(' 🔒 Checking for CVE vulnerabilities...'))
|
|
43
|
+
const cveResult = await cveCheck(code, language)
|
|
44
|
+
const high = cveResult.high_severity || []
|
|
45
|
+
const med = cveResult.medium_severity || []
|
|
46
|
+
if (high.length > 0) {
|
|
47
|
+
console.log(chalk.red(` ⚠ ${high.length} high severity CVEs found!`))
|
|
48
|
+
for (const cve of high) console.log(chalk.red(` [${cve.score}] ${cve.cve_id}: ${cve.description?.slice(0, 80)}`))
|
|
49
|
+
} else if (med.length > 0) {
|
|
50
|
+
console.log(chalk.yellow(` ⚠ ${med.length} medium severity CVEs`))
|
|
51
|
+
} else {
|
|
52
|
+
console.log(chalk.green(' ✓ No known vulnerabilities'))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
spinner.fail(chalk.red(`Error: ${e.message}`))
|
|
57
|
+
}
|
|
58
|
+
console.log()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { runTask }
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { execSync } = require('child_process')
|
|
4
|
+
const chalk = require('chalk')
|
|
5
|
+
|
|
6
|
+
function readFile(filePath) {
|
|
7
|
+
try {
|
|
8
|
+
const resolved = path.resolve(filePath)
|
|
9
|
+
const content = fs.readFileSync(resolved, 'utf8')
|
|
10
|
+
return { success: true, path: resolved, content, lines: content.split('\n').length }
|
|
11
|
+
} catch (e) {
|
|
12
|
+
return { success: false, error: e.message }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function writeFile(filePath, content) {
|
|
17
|
+
try {
|
|
18
|
+
const resolved = path.resolve(filePath)
|
|
19
|
+
const dir = path.dirname(resolved)
|
|
20
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
21
|
+
fs.writeFileSync(resolved, content)
|
|
22
|
+
return { success: true, path: resolved, lines: content.split('\n').length }
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return { success: false, error: e.message }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function editFile(filePath, oldStr, newStr) {
|
|
29
|
+
try {
|
|
30
|
+
const resolved = path.resolve(filePath)
|
|
31
|
+
let content = fs.readFileSync(resolved, 'utf8')
|
|
32
|
+
if (!content.includes(oldStr)) {
|
|
33
|
+
return { success: false, error: 'Old string not found in file' }
|
|
34
|
+
}
|
|
35
|
+
content = content.replace(oldStr, newStr)
|
|
36
|
+
fs.writeFileSync(resolved, content)
|
|
37
|
+
return { success: true, path: resolved }
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return { success: false, error: e.message }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function listFiles(dir = '.', pattern = '') {
|
|
44
|
+
try {
|
|
45
|
+
const resolved = path.resolve(dir)
|
|
46
|
+
const files = []
|
|
47
|
+
function walk(d, depth = 0) {
|
|
48
|
+
if (depth > 4) return
|
|
49
|
+
const entries = fs.readdirSync(d, { withFileTypes: true })
|
|
50
|
+
for (const e of entries) {
|
|
51
|
+
if (e.name.startsWith('.') || e.name === 'node_modules' || e.name === '__pycache__') continue
|
|
52
|
+
const full = path.join(d, e.name)
|
|
53
|
+
const rel = path.relative(resolved, full)
|
|
54
|
+
if (e.isDirectory()) {
|
|
55
|
+
files.push(rel + '/')
|
|
56
|
+
walk(full, depth + 1)
|
|
57
|
+
} else {
|
|
58
|
+
if (!pattern || rel.includes(pattern) || e.name.match(pattern)) files.push(rel)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
walk(resolved)
|
|
63
|
+
return { success: true, files: files.slice(0, 200), total: files.length }
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return { success: false, error: e.message }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function runCommand(cmd, timeout = 15000) {
|
|
70
|
+
try {
|
|
71
|
+
const output = execSync(cmd, { encoding: 'utf8', timeout, maxBuffer: 1024 * 1024 })
|
|
72
|
+
return { success: true, output: output.slice(0, 8000) }
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return { success: false, output: (e.stdout || '').slice(0, 4000), error: (e.stderr || e.message).slice(0, 2000) }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function searchCode(pattern, dir = '.') {
|
|
79
|
+
try {
|
|
80
|
+
const output = execSync(`grep -rn "${pattern}" "${dir}" --include="*.{js,ts,tsx,py,go,rs,java,c,cpp,h,css,html,json,yaml,yml,md,sh}" 2>/dev/null | head -30`, {
|
|
81
|
+
encoding: 'utf8', timeout: 10000
|
|
82
|
+
})
|
|
83
|
+
return { success: true, results: output.trim() }
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return { success: true, results: '' }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Process AI response — extract and apply file operations
|
|
90
|
+
function processToolCalls(response) {
|
|
91
|
+
const actions = []
|
|
92
|
+
|
|
93
|
+
// Extract file writes: ```filename.ext\n...content...\n```
|
|
94
|
+
const fileBlocks = response.matchAll(/```(\S+\.\w+)\n([\s\S]*?)```/g)
|
|
95
|
+
for (const match of fileBlocks) {
|
|
96
|
+
const [, filename, content] = match
|
|
97
|
+
if (filename && !['python', 'javascript', 'typescript', 'bash', 'json', 'html', 'css', 'sql', 'rust', 'go', 'java', 'c', 'cpp'].includes(filename)) {
|
|
98
|
+
actions.push({ type: 'write', path: filename, content })
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return actions
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { readFile, writeFile, editFile, listFiles, runCommand, searchCode, processToolCalls }
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "otoro-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Otoro AGI — AI coding assistant for your terminal. Code, generate images, execute tasks, and control your projects remotely.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"otoro": "./bin/otoro.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": ["ai", "coding", "assistant", "cli", "otoro", "agi", "code-generation", "developer-tools"],
|
|
10
|
+
"author": "Otoro Team",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/otoroagi/cli"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://otoroagi.com",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"chalk": "^4.1.2",
|
|
19
|
+
"commander": "^11.0.0",
|
|
20
|
+
"ora": "^5.4.1",
|
|
21
|
+
"ws": "^8.18.0"
|
|
22
|
+
}
|
|
23
|
+
}
|