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/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 }
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Otoro Screen Agent — Python companion for reliable cross-platform computer use.
4
+ Installed alongside the Node.js CLI. Uses pyautogui for mouse/keyboard.
5
+
6
+ Usage: python screen_agent.py <action> [args...]
7
+ screenshot — capture screen, save to /tmp/otoro-screen.png
8
+ click <x> <y> — click at coordinates
9
+ rightclick <x> <y> — right-click
10
+ doubleclick <x> <y> — double-click
11
+ move <x> <y> — move mouse
12
+ type <text> — type text
13
+ hotkey <key1> <key2> — press key combo (e.g. hotkey ctrl s)
14
+ locate <image_path> — find image on screen, return coordinates
15
+ scroll <amount> — scroll up (positive) or down (negative)
16
+ info — get screen resolution and mouse position
17
+ """
18
+ import sys
19
+ import json
20
+
21
+ def main():
22
+ try:
23
+ import pyautogui
24
+ pyautogui.FAILSAFE = True
25
+ pyautogui.PAUSE = 0.1
26
+ except ImportError:
27
+ print(json.dumps({"error": "pyautogui not installed. Run: pip install pyautogui"}))
28
+ sys.exit(1)
29
+
30
+ if len(sys.argv) < 2:
31
+ print(json.dumps({"error": "No action specified"}))
32
+ sys.exit(1)
33
+
34
+ action = sys.argv[1]
35
+
36
+ try:
37
+ if action == "screenshot":
38
+ path = "/tmp/otoro-screen.png"
39
+ img = pyautogui.screenshot()
40
+ img.save(path)
41
+ print(json.dumps({"success": True, "path": path,
42
+ "size": [img.width, img.height]}))
43
+
44
+ elif action == "click":
45
+ x, y = int(sys.argv[2]), int(sys.argv[3])
46
+ pyautogui.click(x, y)
47
+ print(json.dumps({"success": True, "x": x, "y": y}))
48
+
49
+ elif action == "rightclick":
50
+ x, y = int(sys.argv[2]), int(sys.argv[3])
51
+ pyautogui.rightClick(x, y)
52
+ print(json.dumps({"success": True, "x": x, "y": y}))
53
+
54
+ elif action == "doubleclick":
55
+ x, y = int(sys.argv[2]), int(sys.argv[3])
56
+ pyautogui.doubleClick(x, y)
57
+ print(json.dumps({"success": True, "x": x, "y": y}))
58
+
59
+ elif action == "move":
60
+ x, y = int(sys.argv[2]), int(sys.argv[3])
61
+ pyautogui.moveTo(x, y)
62
+ print(json.dumps({"success": True, "x": x, "y": y}))
63
+
64
+ elif action == "type":
65
+ text = " ".join(sys.argv[2:])
66
+ pyautogui.typewrite(text, interval=0.02)
67
+ print(json.dumps({"success": True, "typed": len(text)}))
68
+
69
+ elif action == "hotkey":
70
+ keys = sys.argv[2:]
71
+ pyautogui.hotkey(*keys)
72
+ print(json.dumps({"success": True, "keys": keys}))
73
+
74
+ elif action == "scroll":
75
+ amount = int(sys.argv[2])
76
+ pyautogui.scroll(amount)
77
+ print(json.dumps({"success": True, "scrolled": amount}))
78
+
79
+ elif action == "info":
80
+ size = pyautogui.size()
81
+ pos = pyautogui.position()
82
+ print(json.dumps({"screen": [size.width, size.height],
83
+ "mouse": [pos.x, pos.y]}))
84
+
85
+ elif action == "locate":
86
+ path = sys.argv[2]
87
+ loc = pyautogui.locateOnScreen(path, confidence=0.8)
88
+ if loc:
89
+ center = pyautogui.center(loc)
90
+ print(json.dumps({"found": True, "x": center.x, "y": center.y,
91
+ "region": [loc.left, loc.top, loc.width, loc.height]}))
92
+ else:
93
+ print(json.dumps({"found": False}))
94
+
95
+ else:
96
+ print(json.dumps({"error": f"Unknown action: {action}"}))
97
+
98
+ except Exception as e:
99
+ print(json.dumps({"error": str(e)}))
100
+
101
+ if __name__ == "__main__":
102
+ main()