otoro-cli 1.4.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/bin/otoro.js +82 -114
- package/lib/agent-loop.js +230 -0
- package/lib/chat.js +92 -176
- package/lib/screen_agent.py +102 -0
- package/lib/tools.js +275 -147
- package/package.json +3 -7
package/bin/otoro.js
CHANGED
|
@@ -1,153 +1,121 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const { program } = require('commander')
|
|
3
|
-
const { initConfig, login, logout, isLoggedIn, requireAuth } = require('../lib/config')
|
|
4
|
-
const chalk = require('chalk')
|
|
5
|
-
const pkg = require('../package.json')
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
program
|
|
13
|
-
.command('login')
|
|
14
|
-
.description('Sign in with your Otoro account')
|
|
15
|
-
.action(async () => {
|
|
16
|
-
await login()
|
|
17
|
-
})
|
|
3
|
+
const { Command } = require('commander')
|
|
4
|
+
const { interactiveChat } = require('../lib/chat')
|
|
5
|
+
const { agentLoop } = require('../lib/agent-loop')
|
|
6
|
+
const { getConfig, login } = require('../lib/config')
|
|
7
|
+
const chalk = require('chalk')
|
|
18
8
|
|
|
19
|
-
program
|
|
20
|
-
.command('logout')
|
|
21
|
-
.description('Sign out and clear credentials')
|
|
22
|
-
.action(async () => {
|
|
23
|
-
await logout()
|
|
24
|
-
})
|
|
9
|
+
const program = new Command()
|
|
25
10
|
|
|
26
11
|
program
|
|
27
|
-
.
|
|
28
|
-
.description('
|
|
29
|
-
.
|
|
30
|
-
await initConfig()
|
|
31
|
-
console.log(chalk.green(' Run `otoro` to start coding.\n'))
|
|
32
|
-
})
|
|
12
|
+
.name('otoro')
|
|
13
|
+
.description('Otoro AGI — AI coding assistant for your terminal')
|
|
14
|
+
.version('2.0.0')
|
|
33
15
|
|
|
16
|
+
// Default — interactive chat (like Claude Code)
|
|
34
17
|
program
|
|
35
|
-
.command('chat')
|
|
36
|
-
.alias('c')
|
|
37
|
-
.description('Start interactive chat')
|
|
38
18
|
.action(async () => {
|
|
39
|
-
|
|
40
|
-
const { startChat } = require('../lib/chat')
|
|
41
|
-
await startChat()
|
|
19
|
+
await interactiveChat()
|
|
42
20
|
})
|
|
43
21
|
|
|
22
|
+
// Ask — one-shot question with agentic tools
|
|
44
23
|
program
|
|
45
|
-
.command('ask <
|
|
46
|
-
.description('Ask Otoro a
|
|
47
|
-
.action(async (
|
|
48
|
-
|
|
49
|
-
const {
|
|
50
|
-
|
|
24
|
+
.command('ask <question...>')
|
|
25
|
+
.description('Ask Otoro a question (uses tools)')
|
|
26
|
+
.action(async (words) => {
|
|
27
|
+
const question = words.join(' ')
|
|
28
|
+
const { response } = await agentLoop(question)
|
|
29
|
+
if (response) console.log('\n' + response + '\n')
|
|
51
30
|
})
|
|
52
31
|
|
|
32
|
+
// Run — full agentic task
|
|
53
33
|
program
|
|
54
34
|
.command('run <task...>')
|
|
55
|
-
.description('Run a
|
|
56
|
-
.action(async (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
await
|
|
35
|
+
.description('Run a task (Otoro uses tools autonomously)')
|
|
36
|
+
.action(async (words) => {
|
|
37
|
+
const task = words.join(' ')
|
|
38
|
+
console.log(chalk.cyan('\n 🐙 Running task: ') + chalk.white(task) + '\n')
|
|
39
|
+
const { response, iterations } = await agentLoop(task)
|
|
40
|
+
if (response) console.log('\n' + response)
|
|
41
|
+
console.log(chalk.gray(`\n Completed in ${iterations} steps\n`))
|
|
60
42
|
})
|
|
61
43
|
|
|
44
|
+
// Login
|
|
62
45
|
program
|
|
63
|
-
.command('
|
|
64
|
-
.description('
|
|
65
|
-
.action(async (
|
|
66
|
-
|
|
67
|
-
const { generateImage } = require('../lib/image')
|
|
68
|
-
await generateImage(prompt.join(' '))
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
program
|
|
72
|
-
.command('computer <task...>')
|
|
73
|
-
.alias('use')
|
|
74
|
-
.description('Computer Use — Otoro sees your screen and controls mouse/keyboard')
|
|
75
|
-
.action(async (task) => {
|
|
76
|
-
requireAuth()
|
|
77
|
-
const { runComputerTask } = require('../lib/computer')
|
|
78
|
-
await runComputerTask(task.join(' '))
|
|
46
|
+
.command('login')
|
|
47
|
+
.description('Login to Otoro')
|
|
48
|
+
.action(async () => {
|
|
49
|
+
await login()
|
|
79
50
|
})
|
|
80
51
|
|
|
52
|
+
// Init — scan project
|
|
81
53
|
program
|
|
82
|
-
.command('
|
|
83
|
-
.description('
|
|
54
|
+
.command('init')
|
|
55
|
+
.description('Initialize Otoro in current project')
|
|
84
56
|
.action(async () => {
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
spinner.succeed(chalk.green('Screen analyzed'))
|
|
93
|
-
console.log(chalk.cyan('\n What Otoro sees:\n'))
|
|
94
|
-
console.log(' ' + result.description.split('\n').join('\n ') + '\n')
|
|
95
|
-
} else {
|
|
96
|
-
spinner.fail(chalk.red(result.error))
|
|
97
|
-
}
|
|
57
|
+
const { executeTool } = require('../lib/tools')
|
|
58
|
+
const result = await executeTool('project_context', {})
|
|
59
|
+
console.log(chalk.cyan('\n 🐙 Otoro initialized'))
|
|
60
|
+
console.log(chalk.gray(` Project type: ${result.type}`))
|
|
61
|
+
console.log(chalk.gray(` Files: ${result.file_count}`))
|
|
62
|
+
console.log(chalk.gray(` Directory: ${result.directory}`))
|
|
63
|
+
console.log(chalk.gray('\n Run `otoro` to start coding\n'))
|
|
98
64
|
})
|
|
99
65
|
|
|
66
|
+
// Status
|
|
100
67
|
program
|
|
101
|
-
.command('
|
|
102
|
-
.description('
|
|
68
|
+
.command('status')
|
|
69
|
+
.description('Show project and agent status')
|
|
103
70
|
.action(async () => {
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
await
|
|
71
|
+
const { executeTool } = require('../lib/tools')
|
|
72
|
+
const git = await executeTool('git_status', {})
|
|
73
|
+
const proj = await executeTool('project_context', {})
|
|
74
|
+
console.log(chalk.cyan('\n 🐙 Otoro Status'))
|
|
75
|
+
console.log(chalk.gray(` Project: ${proj.type} (${proj.file_count} files)`))
|
|
76
|
+
if (!git.error) {
|
|
77
|
+
console.log(chalk.gray(` Branch: ${git.branch}`))
|
|
78
|
+
console.log(chalk.gray(` Git: ${git.status}`))
|
|
79
|
+
}
|
|
80
|
+
console.log('')
|
|
107
81
|
})
|
|
108
82
|
|
|
83
|
+
// Image gen
|
|
109
84
|
program
|
|
110
|
-
.command('
|
|
111
|
-
.description('
|
|
112
|
-
.action(async () => {
|
|
113
|
-
|
|
114
|
-
const config =
|
|
85
|
+
.command('image <prompt...>')
|
|
86
|
+
.description('Generate an image')
|
|
87
|
+
.action(async (words) => {
|
|
88
|
+
const prompt = words.join(' ')
|
|
89
|
+
const config = getConfig()
|
|
115
90
|
const http = require('http')
|
|
116
|
-
const
|
|
117
|
-
|
|
91
|
+
const fs = require('fs')
|
|
92
|
+
|
|
93
|
+
console.log(chalk.cyan('\n 🎨 Generating image...'))
|
|
94
|
+
const body = JSON.stringify({ prompt, size: '1024x1024' })
|
|
95
|
+
const url = new URL(`${config.gpu_url}/v1/images/generations`)
|
|
96
|
+
const req = http.request(url, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.api_key}` },
|
|
99
|
+
timeout: 120000,
|
|
100
|
+
}, (res) => {
|
|
118
101
|
let data = ''
|
|
119
102
|
res.on('data', c => data += c)
|
|
120
103
|
res.on('end', () => {
|
|
121
104
|
try {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
105
|
+
const result = JSON.parse(data)
|
|
106
|
+
if (result.data?.[0]?.b64_json) {
|
|
107
|
+
const filename = result.data[0].filename || `otoro-${Date.now()}.png`
|
|
108
|
+
fs.writeFileSync(filename, Buffer.from(result.data[0].b64_json, 'base64'))
|
|
109
|
+
console.log(chalk.green(` Saved: ${filename}\n`))
|
|
126
110
|
} else {
|
|
127
|
-
|
|
128
|
-
console.log(chalk.green(` ● ${a.hostname}`) + chalk.gray(` — ${a.cwd} (${a.platform})`))
|
|
129
|
-
}
|
|
130
|
-
console.log()
|
|
111
|
+
console.log(chalk.red(' Failed to generate image\n'))
|
|
131
112
|
}
|
|
132
|
-
} catch { console.log(chalk.red('
|
|
113
|
+
} catch { console.log(chalk.red(' Error parsing response\n')) }
|
|
133
114
|
})
|
|
134
|
-
})
|
|
115
|
+
})
|
|
116
|
+
req.on('error', (e) => console.log(chalk.red(` Error: ${e.message}\n`)))
|
|
117
|
+
req.write(body)
|
|
118
|
+
req.end()
|
|
135
119
|
})
|
|
136
120
|
|
|
137
|
-
|
|
138
|
-
if (process.argv.length <= 2) {
|
|
139
|
-
if (!isLoggedIn()) {
|
|
140
|
-
console.log(chalk.cyan.bold('\n 🐙 Otoro AGI\n'))
|
|
141
|
-
console.log(chalk.yellow(' Not logged in. Run:\n'))
|
|
142
|
-
console.log(chalk.white(' otoro login — Sign in with your Otoro account'))
|
|
143
|
-
console.log(chalk.white(' otoro init — Initialize a project'))
|
|
144
|
-
console.log(chalk.white(' otoro — Start coding\n'))
|
|
145
|
-
console.log(chalk.gray(' Don\'t have an account? Sign up at https://otoroagi.com\n'))
|
|
146
|
-
} else {
|
|
147
|
-
requireAuth()
|
|
148
|
-
const { startChat } = require('../lib/chat')
|
|
149
|
-
startChat()
|
|
150
|
-
}
|
|
151
|
-
} else {
|
|
152
|
-
program.parse()
|
|
153
|
-
}
|
|
121
|
+
program.parse()
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
const chalk = require('chalk')
|
|
2
|
+
const http = require('http')
|
|
3
|
+
const { getConfig } = require('./config')
|
|
4
|
+
const { getToolDefinitions, executeTool } = require('./tools')
|
|
5
|
+
|
|
6
|
+
// ═══════════════════════════════════════════════════
|
|
7
|
+
// OTORO AGENTIC LOOP — Claude Code-style tool use
|
|
8
|
+
// ═══════════════════════════════════════════════════
|
|
9
|
+
|
|
10
|
+
const MAX_ITERATIONS = 25
|
|
11
|
+
const SYSTEM_PROMPT = `You are Otoro, an expert AI coding assistant running in the user's terminal.
|
|
12
|
+
You have access to tools that let you read files, write files, edit files, search code, run commands, and more.
|
|
13
|
+
|
|
14
|
+
IMPORTANT RULES:
|
|
15
|
+
- Read files before editing them. Never guess file contents.
|
|
16
|
+
- Use glob/grep to find files before reading them.
|
|
17
|
+
- Run tests after making changes to verify they work.
|
|
18
|
+
- Be concise in your responses. Show what you did, not what you plan to do.
|
|
19
|
+
- When you're done with a task, stop calling tools and respond to the user.
|
|
20
|
+
- Never ask the user to do something you can do with tools.
|
|
21
|
+
- Use bash for git, npm, pip, build commands, etc.
|
|
22
|
+
- Always use the edit tool for small changes (not write).
|
|
23
|
+
- Use project_context to understand the project before making changes.
|
|
24
|
+
|
|
25
|
+
Current directory: ${process.cwd()}
|
|
26
|
+
Platform: ${process.platform}
|
|
27
|
+
`
|
|
28
|
+
|
|
29
|
+
async function callModel(messages, tools) {
|
|
30
|
+
const config = getConfig()
|
|
31
|
+
const body = JSON.stringify({
|
|
32
|
+
model: 'otoro-1',
|
|
33
|
+
messages,
|
|
34
|
+
tools,
|
|
35
|
+
max_tokens: 8192,
|
|
36
|
+
temperature: 0.2,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const url = new URL(`${config.gpu_url}/v1/chat/completions`)
|
|
41
|
+
const req = http.request(url, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
'Authorization': `Bearer ${config.api_key}`,
|
|
46
|
+
},
|
|
47
|
+
timeout: 180000,
|
|
48
|
+
}, (res) => {
|
|
49
|
+
let data = ''
|
|
50
|
+
res.on('data', c => data += c)
|
|
51
|
+
res.on('end', () => {
|
|
52
|
+
try {
|
|
53
|
+
const result = JSON.parse(data)
|
|
54
|
+
if (result.error) reject(new Error(result.error.message || JSON.stringify(result.error)))
|
|
55
|
+
else resolve(result)
|
|
56
|
+
} catch (e) {
|
|
57
|
+
reject(new Error('Invalid response from model'))
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
req.on('error', reject)
|
|
62
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')) })
|
|
63
|
+
req.write(body)
|
|
64
|
+
req.end()
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatToolResult(name, result) {
|
|
69
|
+
const str = typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
|
70
|
+
// Truncate very large results
|
|
71
|
+
if (str.length > 15000) {
|
|
72
|
+
return str.slice(0, 14000) + '\n\n... (truncated, ' + str.length + ' chars total)'
|
|
73
|
+
}
|
|
74
|
+
return str
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function printToolCall(name, args) {
|
|
78
|
+
const icons = {
|
|
79
|
+
read: '📖', write: '✏️ ', edit: '🔧', glob: '🔍', grep: '🔎',
|
|
80
|
+
bash: '💻', git_status: '📊', project_context: '📁', web_search: '🌐',
|
|
81
|
+
}
|
|
82
|
+
const icon = icons[name] || '🔧'
|
|
83
|
+
|
|
84
|
+
switch (name) {
|
|
85
|
+
case 'read':
|
|
86
|
+
console.log(chalk.cyan(` ${icon} Read ${chalk.white(args.file_path)}`))
|
|
87
|
+
break
|
|
88
|
+
case 'write':
|
|
89
|
+
console.log(chalk.green(` ${icon} Write ${chalk.white(args.file_path)} (${args.content?.length || 0} chars)`))
|
|
90
|
+
break
|
|
91
|
+
case 'edit':
|
|
92
|
+
console.log(chalk.yellow(` ${icon} Edit ${chalk.white(args.file_path)}`))
|
|
93
|
+
break
|
|
94
|
+
case 'glob':
|
|
95
|
+
console.log(chalk.blue(` ${icon} Glob ${chalk.white(args.pattern)} ${args.directory ? 'in ' + args.directory : ''}`))
|
|
96
|
+
break
|
|
97
|
+
case 'grep':
|
|
98
|
+
console.log(chalk.blue(` ${icon} Grep ${chalk.white(args.pattern)} ${args.glob ? '(' + args.glob + ')' : ''}`))
|
|
99
|
+
break
|
|
100
|
+
case 'bash':
|
|
101
|
+
const cmd = args.command.length > 80 ? args.command.slice(0, 77) + '...' : args.command
|
|
102
|
+
console.log(chalk.magenta(` ${icon} ${chalk.white(cmd)}`))
|
|
103
|
+
break
|
|
104
|
+
case 'git_status':
|
|
105
|
+
console.log(chalk.cyan(` ${icon} Git status`))
|
|
106
|
+
break
|
|
107
|
+
case 'project_context':
|
|
108
|
+
console.log(chalk.cyan(` ${icon} Scanning project`))
|
|
109
|
+
break
|
|
110
|
+
case 'web_search':
|
|
111
|
+
console.log(chalk.blue(` ${icon} Search: ${chalk.white(args.query)}`))
|
|
112
|
+
break
|
|
113
|
+
default:
|
|
114
|
+
console.log(chalk.gray(` ${icon} ${name}`))
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function agentLoop(userMessage, conversationHistory = []) {
|
|
119
|
+
const tools = getToolDefinitions()
|
|
120
|
+
const messages = [
|
|
121
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
122
|
+
...conversationHistory,
|
|
123
|
+
{ role: 'user', content: userMessage },
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
let iteration = 0
|
|
127
|
+
let finalResponse = ''
|
|
128
|
+
|
|
129
|
+
while (iteration < MAX_ITERATIONS) {
|
|
130
|
+
iteration++
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const result = await callModel(messages, tools)
|
|
134
|
+
const choice = result.choices?.[0]
|
|
135
|
+
|
|
136
|
+
if (!choice) {
|
|
137
|
+
console.log(chalk.red(' No response from model'))
|
|
138
|
+
break
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const message = choice.message
|
|
142
|
+
|
|
143
|
+
// Check for tool calls
|
|
144
|
+
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
145
|
+
// Add assistant message with tool calls to history
|
|
146
|
+
messages.push({
|
|
147
|
+
role: 'assistant',
|
|
148
|
+
content: message.content || null,
|
|
149
|
+
tool_calls: message.tool_calls,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// Print any text content the model included
|
|
153
|
+
if (message.content) {
|
|
154
|
+
console.log(chalk.white(message.content))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Execute each tool call
|
|
158
|
+
for (const tc of message.tool_calls) {
|
|
159
|
+
const toolName = tc.function.name
|
|
160
|
+
let toolArgs = {}
|
|
161
|
+
try {
|
|
162
|
+
toolArgs = JSON.parse(tc.function.arguments || '{}')
|
|
163
|
+
} catch {
|
|
164
|
+
toolArgs = {}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Print tool call
|
|
168
|
+
printToolCall(toolName, toolArgs)
|
|
169
|
+
|
|
170
|
+
// Execute
|
|
171
|
+
const toolResult = await executeTool(toolName, toolArgs)
|
|
172
|
+
const formatted = formatToolResult(toolName, toolResult)
|
|
173
|
+
|
|
174
|
+
// Print errors
|
|
175
|
+
if (toolResult.error) {
|
|
176
|
+
console.log(chalk.red(` Error: ${toolResult.error}`))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Add tool result to messages
|
|
180
|
+
messages.push({
|
|
181
|
+
role: 'tool',
|
|
182
|
+
tool_call_id: tc.id,
|
|
183
|
+
content: formatted,
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Continue the loop — model will see tool results and decide what to do next
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// No tool calls — model is done, this is the final response
|
|
192
|
+
finalResponse = message.content || ''
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.log(chalk.red(` Error: ${err.message}`))
|
|
197
|
+
// If model doesn't support tool_calls, fall back to plain response
|
|
198
|
+
if (err.message.includes('tool') || err.message.includes('function')) {
|
|
199
|
+
// Retry without tools
|
|
200
|
+
try {
|
|
201
|
+
const result = await callModel(messages.map(m => {
|
|
202
|
+
const { tool_calls, ...rest } = m
|
|
203
|
+
return rest
|
|
204
|
+
}).filter(m => m.role !== 'tool'), null)
|
|
205
|
+
finalResponse = result.choices?.[0]?.message?.content || ''
|
|
206
|
+
} catch {
|
|
207
|
+
finalResponse = 'Sorry, I encountered an error. Please try again.'
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
break
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (iteration >= MAX_ITERATIONS) {
|
|
215
|
+
console.log(chalk.yellow(`\n Reached max iterations (${MAX_ITERATIONS})`))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Add final response to conversation history
|
|
219
|
+
if (finalResponse) {
|
|
220
|
+
messages.push({ role: 'assistant', content: finalResponse })
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
response: finalResponse,
|
|
225
|
+
iterations: iteration,
|
|
226
|
+
messages, // Return full history for multi-turn
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = { agentLoop }
|