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 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
+ }