otoro-cli 1.5.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 }
package/lib/chat.js CHANGED
@@ -1,217 +1,133 @@
1
1
  const readline = require('readline')
2
2
  const chalk = require('chalk')
3
- const ora = require('ora')
3
+ const { agentLoop } = require('./agent-loop')
4
+ const { getConfig } = require('./config')
4
5
  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')
6
+ const os = require('os')
8
7
 
9
- const SYSTEM_PROMPT = `You are Otoro — an expert AI coding assistant running in the user's terminal.
8
+ async function interactiveChat() {
9
+ const config = getConfig()
10
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()}`))
11
+ if (!config.api_key) {
12
+ console.log(chalk.red('\n Not logged in. Run: otoro login\n'))
13
+ process.exit(1)
14
+ }
37
15
 
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'))
16
+ console.log('')
17
+ console.log(chalk.bold(' 🐙 Otoro AGI'))
18
+ console.log(chalk.gray(` ${process.cwd()}`))
19
+ console.log(chalk.gray(` Model: otoro-1 | Tools: read, write, edit, glob, grep, bash, git`))
20
+ console.log(chalk.gray(` Type /help for commands, Ctrl+C to exit`))
21
+ console.log('')
43
22
 
44
23
  const rl = readline.createInterface({
45
24
  input: process.stdin,
46
25
  output: process.stdout,
47
- prompt: chalk.green(' ❯ '),
26
+ prompt: chalk.cyan(' ❯ '),
48
27
  })
49
28
 
29
+ let conversationHistory = []
30
+ let compactMode = false
31
+
50
32
  rl.prompt()
51
33
 
52
- rl.on('line', async (input) => {
53
- const line = input.trim()
54
- if (!line) { rl.prompt(); return }
34
+ rl.on('line', async (line) => {
35
+ const input = line.trim()
36
+ if (!input) { rl.prompt(); return }
55
37
 
56
- if (line === '/help') {
57
- console.log(chalk.cyan(`
38
+ // Commands
39
+ if (input === '/help') {
40
+ console.log(chalk.gray(`
58
41
  Commands:
59
42
  /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
43
+ /clear Clear conversation history
44
+ /compact Toggle compact mode
45
+ /context Scan project structure
46
+ /status Git status
64
47
  /exit Exit Otoro
65
- `))
48
+ `))
66
49
  rl.prompt(); return
67
50
  }
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`))
51
+ if (input === '/exit' || input === '/quit') { process.exit(0) }
52
+ if (input === '/clear') {
53
+ conversationHistory = []
54
+ console.log(chalk.gray(' Conversation cleared'))
73
55
  rl.prompt(); return
74
56
  }
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}`))
57
+ if (input === '/compact') {
58
+ compactMode = !compactMode
59
+ console.log(chalk.gray(` Compact mode: ${compactMode ? 'on' : 'off'}`))
79
60
  rl.prompt(); return
80
61
  }
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}`))
62
+ if (input === '/context') {
63
+ const { executeTool } = require('./tools')
64
+ const result = await executeTool('project_context', {})
65
+ console.log(chalk.cyan(`\n Project: ${result.type}`))
66
+ console.log(chalk.gray(` Files: ${result.file_count}`))
67
+ if (result.package_json?.name) console.log(chalk.gray(` Name: ${result.package_json.name}`))
68
+ console.log('')
84
69
  rl.prompt(); return
85
70
  }
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
- }
71
+ if (input === '/status') {
72
+ const { executeTool } = require('./tools')
73
+ const result = await executeTool('git_status', {})
74
+ if (result.error) console.log(chalk.red(` ${result.error}`))
75
+ else {
76
+ console.log(chalk.cyan(`\n Branch: ${result.branch}`))
77
+ console.log(chalk.gray(` ${result.status}`))
78
+ console.log(chalk.gray(` ${result.recent_commits}`))
96
79
  }
80
+ console.log('')
81
+ rl.prompt(); return
97
82
  }
98
83
 
99
- history.push({ role: 'user', content: contextMsg })
100
-
101
- const spinner = ora({ text: chalk.gray('Thinking...'), color: 'cyan' }).start()
84
+ // Pause readline while processing
85
+ rl.pause()
86
+ console.log('')
102
87
 
103
88
  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()
89
+ const { response, iterations, messages } = await agentLoop(input, conversationHistory)
108
90
 
109
- history.push({ role: 'assistant', content: response })
91
+ // Update conversation history (keep last 20 messages to manage context)
92
+ conversationHistory = messages.slice(1) // Remove system prompt
93
+ if (conversationHistory.length > 40) {
94
+ conversationHistory = conversationHistory.slice(-30)
95
+ }
110
96
 
111
- // Process tool calls in response
112
- await processResponse(response)
97
+ // Print response
98
+ if (response) {
99
+ console.log('')
100
+ // Format markdown-style output
101
+ const lines = response.split('\n')
102
+ for (const line of lines) {
103
+ if (line.startsWith('```')) {
104
+ console.log(chalk.gray(` ${line}`))
105
+ } else if (line.startsWith('#')) {
106
+ console.log(chalk.bold.white(` ${line}`))
107
+ } else if (line.startsWith('- ') || line.startsWith('* ')) {
108
+ console.log(chalk.white(` ${line}`))
109
+ } else {
110
+ console.log(chalk.white(` ${line}`))
111
+ }
112
+ }
113
113
 
114
- } catch (e) {
115
- spinner.fail(chalk.red(`Error: ${e.message}`))
114
+ if (iterations > 1) {
115
+ console.log(chalk.gray(`\n (${iterations} steps)`))
116
+ }
117
+ }
118
+ } catch (err) {
119
+ console.log(chalk.red(` Error: ${err.message}`))
116
120
  }
117
121
 
122
+ console.log('')
123
+ rl.resume()
118
124
  rl.prompt()
119
125
  })
120
- }
121
126
 
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
- }
127
+ rl.on('close', () => {
128
+ console.log(chalk.gray('\n Goodbye! 🐙\n'))
129
+ process.exit(0)
130
+ })
215
131
  }
216
132
 
217
- module.exports = { startChat, askOnce }
133
+ module.exports = { interactiveChat }
package/lib/tools.js CHANGED
@@ -1,173 +1,301 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
- const { execSync } = require('child_process')
4
- const chalk = require('chalk')
3
+ const { execSync, exec } = require('child_process')
4
+ const os = require('os')
5
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
- }
6
+ // ═══════════════════════════════════════════════════
7
+ // OTORO TOOL SYSTEM — Claude Code-style agentic tools
8
+ // ═══════════════════════════════════════════════════
15
9
 
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
- }
10
+ const TOOLS = {
11
+ // ─── File System ────────────────────────────────
12
+ read: {
13
+ name: 'read',
14
+ description: 'Read a file from the filesystem',
15
+ parameters: { file_path: 'string', offset: 'number?', limit: 'number?' },
16
+ execute({ file_path, offset, limit }) {
17
+ const abs = path.resolve(file_path)
18
+ if (!fs.existsSync(abs)) return { error: `File not found: ${abs}` }
19
+ const stat = fs.statSync(abs)
20
+ if (stat.isDirectory()) return { error: `${abs} is a directory, not a file` }
21
+ if (stat.size > 5 * 1024 * 1024) return { error: `File too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB` }
22
+ const content = fs.readFileSync(abs, 'utf8')
23
+ const lines = content.split('\n')
24
+ const start = (offset || 1) - 1
25
+ const end = limit ? start + limit : lines.length
26
+ const numbered = lines.slice(start, end).map((l, i) => `${start + i + 1}\t${l}`).join('\n')
27
+ return { content: numbered, total_lines: lines.length, path: abs }
28
+ }
29
+ },
27
30
 
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' }
31
+ write: {
32
+ name: 'write',
33
+ description: 'Write content to a file (creates or overwrites)',
34
+ parameters: { file_path: 'string', content: 'string' },
35
+ execute({ file_path, content }) {
36
+ const abs = path.resolve(file_path)
37
+ const dir = path.dirname(abs)
38
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
39
+ fs.writeFileSync(abs, content, 'utf8')
40
+ return { success: true, path: abs, bytes: Buffer.byteLength(content) }
34
41
  }
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
+ },
42
43
 
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
- }
44
+ edit: {
45
+ name: 'edit',
46
+ description: 'Replace exact string in a file',
47
+ parameters: { file_path: 'string', old_string: 'string', new_string: 'string' },
48
+ execute({ file_path, old_string, new_string }) {
49
+ const abs = path.resolve(file_path)
50
+ if (!fs.existsSync(abs)) return { error: `File not found: ${abs}` }
51
+ let content = fs.readFileSync(abs, 'utf8')
52
+ const count = content.split(old_string).length - 1
53
+ if (count === 0) return { error: `String not found in ${abs}` }
54
+ if (count > 1) return { error: `String found ${count} times — be more specific` }
55
+ content = content.replace(old_string, new_string)
56
+ fs.writeFileSync(abs, content, 'utf8')
57
+ return { success: true, path: abs, replacements: 1 }
61
58
  }
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
- }
59
+ },
68
60
 
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
- }
61
+ // ─── Search ─────────────────────────────────────
62
+ glob: {
63
+ name: 'glob',
64
+ description: 'Find files matching a glob pattern',
65
+ parameters: { pattern: 'string', directory: 'string?' },
66
+ execute({ pattern, directory }) {
67
+ const dir = directory || process.cwd()
68
+ try {
69
+ // Use find for glob-like matching
70
+ const cmd = `find ${JSON.stringify(dir)} -name ${JSON.stringify(pattern)} -type f 2>/dev/null | head -50`
71
+ const result = execSync(cmd, { encoding: 'utf8', timeout: 10000 }).trim()
72
+ const files = result ? result.split('\n') : []
73
+ return { files, count: files.length }
74
+ } catch {
75
+ return { files: [], count: 0 }
76
+ }
77
+ }
78
+ },
77
79
 
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
- }
80
+ grep: {
81
+ name: 'grep',
82
+ description: 'Search file contents for a pattern (regex supported)',
83
+ parameters: { pattern: 'string', directory: 'string?', glob: 'string?' },
84
+ execute({ pattern, directory, glob: fileGlob }) {
85
+ const dir = directory || process.cwd()
86
+ try {
87
+ let cmd = `grep -rn --include="${fileGlob || '*'}" ${JSON.stringify(pattern)} ${JSON.stringify(dir)} 2>/dev/null | head -30`
88
+ const result = execSync(cmd, { encoding: 'utf8', timeout: 10000 }).trim()
89
+ const matches = result ? result.split('\n').map(line => {
90
+ const [fileLine, ...rest] = line.split(':')
91
+ const parts = fileLine.match(/^(.+):(\d+)$/)
92
+ return parts ? { file: parts[1], line: parseInt(parts[2]), content: rest.join(':').trim() } : { raw: line }
93
+ }) : []
94
+ return { matches, count: matches.length }
95
+ } catch {
96
+ return { matches: [], count: 0 }
97
+ }
98
+ }
99
+ },
88
100
 
89
- // Process AI response — extract and apply file operations
90
- function processToolCalls(response) {
91
- const actions = []
101
+ // ─── Shell ──────────────────────────────────────
102
+ bash: {
103
+ name: 'bash',
104
+ description: 'Execute a bash command',
105
+ parameters: { command: 'string', timeout: 'number?' },
106
+ execute({ command, timeout }) {
107
+ const timeoutMs = Math.min((timeout || 30) * 1000, 120000)
108
+ try {
109
+ const result = execSync(command, {
110
+ encoding: 'utf8',
111
+ timeout: timeoutMs,
112
+ maxBuffer: 1024 * 1024,
113
+ cwd: process.cwd(),
114
+ env: { ...process.env, TERM: 'dumb' },
115
+ })
116
+ return { stdout: result.slice(0, 10000), exit_code: 0 }
117
+ } catch (e) {
118
+ return {
119
+ stdout: (e.stdout || '').slice(0, 5000),
120
+ stderr: (e.stderr || '').slice(0, 5000),
121
+ exit_code: e.status || 1,
122
+ }
123
+ }
124
+ }
125
+ },
92
126
 
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 })
127
+ // ─── Git ────────────────────────────────────────
128
+ git_status: {
129
+ name: 'git_status',
130
+ description: 'Get git status, diff, and recent log',
131
+ parameters: {},
132
+ execute() {
133
+ try {
134
+ const status = execSync('git status --short 2>/dev/null', { encoding: 'utf8', timeout: 5000 }).trim()
135
+ const branch = execSync('git branch --show-current 2>/dev/null', { encoding: 'utf8', timeout: 5000 }).trim()
136
+ const log = execSync('git log --oneline -5 2>/dev/null', { encoding: 'utf8', timeout: 5000 }).trim()
137
+ return { branch, status: status || '(clean)', recent_commits: log }
138
+ } catch {
139
+ return { error: 'Not a git repository' }
140
+ }
99
141
  }
100
- }
142
+ },
101
143
 
102
- return actions
103
- }
144
+ // ─── Project Context ────────────────────────────
145
+ project_context: {
146
+ name: 'project_context',
147
+ description: 'Scan project structure and detect type',
148
+ parameters: { directory: 'string?' },
149
+ execute({ directory }) {
150
+ const dir = directory || process.cwd()
151
+ const files = []
152
+ try {
153
+ const ls = execSync(`find ${JSON.stringify(dir)} -maxdepth 3 -type f -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/__pycache__/*" -not -path "*/dist/*" -not -path "*/.next/*" 2>/dev/null | head -100`, { encoding: 'utf8', timeout: 10000 })
154
+ files.push(...ls.trim().split('\n').filter(Boolean))
155
+ } catch {}
104
156
 
105
- // Platform-aware app launching
106
- function openApp(appName) {
107
- const platform = process.platform
108
- try {
109
- let cmd
110
- if (platform === 'darwin') {
111
- cmd = `open -a "${appName}"`
112
- } else if (platform === 'win32') {
113
- cmd = `start "" "${appName}"`
114
- } else {
115
- // Linux try common approaches
116
- cmd = `${appName.toLowerCase()} &`
157
+ // Detect project type
158
+ const has = (f) => files.some(p => p.endsWith(f))
159
+ let type = 'unknown'
160
+ if (has('package.json')) type = has('next.config.js') || has('next.config.ts') ? 'nextjs' : has('tsconfig.json') ? 'typescript' : 'nodejs'
161
+ else if (has('requirements.txt') || has('setup.py') || has('pyproject.toml')) type = 'python'
162
+ else if (has('Cargo.toml')) type = 'rust'
163
+ else if (has('go.mod')) type = 'go'
164
+ else if (has('Gemfile')) type = 'ruby'
165
+
166
+ // Read key files
167
+ const readme = files.find(f => f.toLowerCase().includes('readme'))
168
+ const pkg = files.find(f => f.endsWith('package.json') && !f.includes('node_modules'))
169
+
170
+ return {
171
+ type,
172
+ directory: dir,
173
+ file_count: files.length,
174
+ files: files.map(f => path.relative(dir, f)).slice(0, 60),
175
+ package_json: pkg ? JSON.parse(fs.readFileSync(pkg, 'utf8').slice(0, 2000)) : null,
176
+ }
117
177
  }
118
- execSync(cmd, { timeout: 5000, stdio: 'ignore' })
119
- return { success: true, app: appName, platform }
120
- } catch (e) {
121
- return { success: false, error: e.message, platform }
122
- }
123
- }
178
+ },
124
179
 
125
- function openUrl(url) {
126
- const platform = process.platform
127
- try {
128
- let cmd
129
- if (platform === 'darwin') cmd = `open "${url}"`
130
- else if (platform === 'win32') cmd = `start "" "${url}"`
131
- else cmd = `xdg-open "${url}"`
132
- execSync(cmd, { timeout: 5000, stdio: 'ignore' })
133
- return { success: true, url }
134
- } catch (e) {
135
- return { success: false, error: e.message }
136
- }
180
+ // ─── Web Search ─────────────────────────────────
181
+ web_search: {
182
+ name: 'web_search',
183
+ description: 'Search the web for information',
184
+ parameters: { query: 'string' },
185
+ async execute({ query }) {
186
+ const { getConfig } = require('./config')
187
+ const config = getConfig()
188
+ const http = require('http')
189
+ return new Promise((resolve) => {
190
+ const body = JSON.stringify({ query, max_results: 5 })
191
+ const url = new URL(`${config.gpu_url}/v1/agent/search`)
192
+ const req = http.request(url, {
193
+ method: 'POST',
194
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.api_key}` },
195
+ timeout: 10000,
196
+ }, (res) => {
197
+ let data = ''
198
+ res.on('data', c => data += c)
199
+ res.on('end', () => {
200
+ try { resolve(JSON.parse(data)) } catch { resolve({ error: 'Bad response' }) }
201
+ })
202
+ })
203
+ req.on('error', () => resolve({ error: 'Search unavailable' }))
204
+ req.write(body)
205
+ req.end()
206
+ })
207
+ }
208
+ },
137
209
  }
138
210
 
139
- function getSystemInfo() {
140
- const os = require('os')
141
- return {
142
- platform: process.platform,
143
- arch: process.arch,
144
- hostname: os.hostname(),
145
- user: os.userInfo().username,
146
- home: os.homedir(),
147
- cwd: process.cwd(),
148
- node: process.version,
149
- cpus: os.cpus().length,
150
- memory: Math.round(os.totalmem() / 1024 / 1024 / 1024) + 'GB',
151
- freeMemory: Math.round(os.freemem() / 1024 / 1024 / 1024) + 'GB',
152
- }
211
+ // ─── Tool definitions for the model ───────────────
212
+ function getToolDefinitions() {
213
+ return [
214
+ {
215
+ type: 'function',
216
+ function: {
217
+ name: 'read',
218
+ description: 'Read a file. Returns numbered lines.',
219
+ parameters: { type: 'object', properties: { file_path: { type: 'string' }, offset: { type: 'integer' }, limit: { type: 'integer' } }, required: ['file_path'] }
220
+ }
221
+ },
222
+ {
223
+ type: 'function',
224
+ function: {
225
+ name: 'write',
226
+ description: 'Write/create a file.',
227
+ parameters: { type: 'object', properties: { file_path: { type: 'string' }, content: { type: 'string' } }, required: ['file_path', 'content'] }
228
+ }
229
+ },
230
+ {
231
+ type: 'function',
232
+ function: {
233
+ name: 'edit',
234
+ description: 'Replace an exact string in a file. old_string must be unique.',
235
+ parameters: { type: 'object', properties: { file_path: { type: 'string' }, old_string: { type: 'string' }, new_string: { type: 'string' } }, required: ['file_path', 'old_string', 'new_string'] }
236
+ }
237
+ },
238
+ {
239
+ type: 'function',
240
+ function: {
241
+ name: 'glob',
242
+ description: 'Find files by name pattern (e.g. "*.tsx", "*.py").',
243
+ parameters: { type: 'object', properties: { pattern: { type: 'string' }, directory: { type: 'string' } }, required: ['pattern'] }
244
+ }
245
+ },
246
+ {
247
+ type: 'function',
248
+ function: {
249
+ name: 'grep',
250
+ description: 'Search file contents for a regex pattern.',
251
+ parameters: { type: 'object', properties: { pattern: { type: 'string' }, directory: { type: 'string' }, glob: { type: 'string' } }, required: ['pattern'] }
252
+ }
253
+ },
254
+ {
255
+ type: 'function',
256
+ function: {
257
+ name: 'bash',
258
+ description: 'Run a shell command. Use for: npm, git, python, build, test, etc.',
259
+ parameters: { type: 'object', properties: { command: { type: 'string' }, timeout: { type: 'integer' } }, required: ['command'] }
260
+ }
261
+ },
262
+ {
263
+ type: 'function',
264
+ function: {
265
+ name: 'git_status',
266
+ description: 'Get git branch, status, and recent commits.',
267
+ parameters: { type: 'object', properties: {} }
268
+ }
269
+ },
270
+ {
271
+ type: 'function',
272
+ function: {
273
+ name: 'project_context',
274
+ description: 'Scan project structure: file list, type detection, package.json.',
275
+ parameters: { type: 'object', properties: { directory: { type: 'string' } } }
276
+ }
277
+ },
278
+ {
279
+ type: 'function',
280
+ function: {
281
+ name: 'web_search',
282
+ description: 'Search the web for current information.',
283
+ parameters: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] }
284
+ }
285
+ },
286
+ ]
153
287
  }
154
288
 
155
- function takeScreenshot() {
156
- const platform = process.platform
157
- const tmpFile = path.join(require('os').tmpdir(), `otoro-screenshot-${Date.now()}.png`)
289
+ // ─── Execute a tool call ──────────────────────────
290
+ async function executeTool(name, args) {
291
+ const tool = TOOLS[name]
292
+ if (!tool) return { error: `Unknown tool: ${name}` }
158
293
  try {
159
- let cmd
160
- if (platform === 'darwin') cmd = `screencapture -x "${tmpFile}"`
161
- else if (platform === 'win32') cmd = `powershell -command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Screen]::PrimaryScreen | ForEach-Object { $bitmap = New-Object System.Drawing.Bitmap($_.Bounds.Width, $_.Bounds.Height); $graphics = [System.Drawing.Graphics]::FromImage($bitmap); $graphics.CopyFromScreen($_.Bounds.Location, [System.Drawing.Point]::Empty, $_.Bounds.Size); $bitmap.Save('${tmpFile}'); }"`
162
- else cmd = `import -window root "${tmpFile}" 2>/dev/null || gnome-screenshot -f "${tmpFile}" 2>/dev/null || scrot "${tmpFile}" 2>/dev/null`
163
- execSync(cmd, { timeout: 10000 })
164
- if (fs.existsSync(tmpFile)) {
165
- return { success: true, path: tmpFile, size: fs.statSync(tmpFile).size }
166
- }
167
- return { success: false, error: 'Screenshot file not created' }
294
+ const result = await tool.execute(args)
295
+ return result
168
296
  } catch (e) {
169
- return { success: false, error: e.message }
297
+ return { error: e.message }
170
298
  }
171
299
  }
172
300
 
173
- module.exports = { readFile, writeFile, editFile, listFiles, runCommand, searchCode, processToolCalls, openApp, openUrl, getSystemInfo, takeScreenshot }
301
+ module.exports = { TOOLS, getToolDefinitions, executeTool }
package/package.json CHANGED
@@ -1,18 +1,14 @@
1
1
  {
2
2
  "name": "otoro-cli",
3
- "version": "1.5.0",
4
- "description": "Otoro AGI — AI coding assistant for your terminal. Code, generate images, execute tasks, and control your projects remotely.",
3
+ "version": "2.0.0",
4
+ "description": "Otoro AGI — AI coding assistant for your terminal. Like Claude Code, but open.",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "otoro": "./bin/otoro.js"
8
8
  },
9
- "keywords": ["ai", "coding", "assistant", "cli", "otoro", "agi", "code-generation", "developer-tools"],
9
+ "keywords": ["ai", "coding", "assistant", "cli", "otoro", "agi", "code-generation", "developer-tools", "claude-code"],
10
10
  "author": "Otoro Team",
11
11
  "license": "MIT",
12
- "repository": {
13
- "type": "git",
14
- "url": "https://github.com/otoroagi/cli"
15
- },
16
12
  "homepage": "https://otoroagi.com",
17
13
  "dependencies": {
18
14
  "chalk": "^4.1.2",