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/lib/chat.js
CHANGED
|
@@ -1,217 +1,133 @@
|
|
|
1
1
|
const readline = require('readline')
|
|
2
2
|
const chalk = require('chalk')
|
|
3
|
-
const
|
|
3
|
+
const { agentLoop } = require('./agent-loop')
|
|
4
|
+
const { getConfig } = require('./config')
|
|
4
5
|
const path = require('path')
|
|
5
|
-
const
|
|
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
|
-
|
|
8
|
+
async function interactiveChat() {
|
|
9
|
+
const config = getConfig()
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
console.log(chalk.gray(`
|
|
42
|
-
console.log(chalk.gray(
|
|
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.
|
|
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 (
|
|
53
|
-
const
|
|
54
|
-
if (!
|
|
34
|
+
rl.on('line', async (line) => {
|
|
35
|
+
const input = line.trim()
|
|
36
|
+
if (!input) { rl.prompt(); return }
|
|
55
37
|
|
|
56
|
-
|
|
57
|
-
|
|
38
|
+
// Commands
|
|
39
|
+
if (input === '/help') {
|
|
40
|
+
console.log(chalk.gray(`
|
|
58
41
|
Commands:
|
|
59
42
|
/help Show this help
|
|
60
|
-
/
|
|
61
|
-
/
|
|
62
|
-
/
|
|
63
|
-
/
|
|
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 (
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
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 (
|
|
76
|
-
|
|
77
|
-
|
|
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 (
|
|
82
|
-
const
|
|
83
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
84
|
+
// Pause readline while processing
|
|
85
|
+
rl.pause()
|
|
86
|
+
console.log('')
|
|
102
87
|
|
|
103
88
|
try {
|
|
104
|
-
const
|
|
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
|
|
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
|
-
//
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 = {
|
|
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()
|