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 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
- program
8
- .name('otoro')
9
- .description('Otoro AGI AI coding assistant')
10
- .version(pkg.version)
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
- .command('init')
28
- .description('Initialize Otoro in this project')
29
- .action(async () => {
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
- requireAuth()
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 <prompt...>')
46
- .description('Ask Otoro a single question')
47
- .action(async (prompt) => {
48
- requireAuth()
49
- const { askOnce } = require('../lib/chat')
50
- await askOnce(prompt.join(' '))
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 coding task (write + execute + verify)')
56
- .action(async (task) => {
57
- requireAuth()
58
- const { runTask } = require('../lib/task')
59
- await runTask(task.join(' '))
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('image <prompt...>')
64
- .description('Generate an image')
65
- .action(async (prompt) => {
66
- requireAuth()
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('screen')
83
- .description('Take a screenshot and describe what Otoro sees')
54
+ .command('init')
55
+ .description('Initialize Otoro in current project')
84
56
  .action(async () => {
85
- requireAuth()
86
- const { analyzeScreen } = require('../lib/screen')
87
- const chalk = require('chalk')
88
- const ora = require('ora')
89
- const spinner = ora({ text: chalk.gray('Looking at screen...'), color: 'cyan' }).start()
90
- const result = await analyzeScreen()
91
- if (result.success) {
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('start')
102
- .description('Start Otoro agent daemon — connects to server for remote tasks')
68
+ .command('status')
69
+ .description('Show project and agent status')
103
70
  .action(async () => {
104
- requireAuth()
105
- const { startAgent } = require('../lib/agent')
106
- await startAgent()
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('status')
111
- .description('Check connected agents and server status')
112
- .action(async () => {
113
- requireAuth()
114
- const config = require('../lib/config').getConfig()
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 url = `${config.gpu_url}/v1/agents`
117
- http.get(url, { headers: { 'Authorization': `Bearer ${config.api_key}` } }, (res) => {
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 d = JSON.parse(data)
123
- console.log(chalk.cyan.bold('\n 🐙 Connected Agents:\n'))
124
- if (d.agents?.length === 0) {
125
- console.log(chalk.gray(' No agents connected. Run `otoro start` on your machine.\n'))
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
- for (const a of d.agents) {
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(' Failed to fetch status')) }
113
+ } catch { console.log(chalk.red(' Error parsing response\n')) }
133
114
  })
134
- }).on('error', (e) => console.log(chalk.red(` Error: ${e.message}`)))
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
- // Default: no args → check login then start chat
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 }