otoro-cli 1.1.0 → 1.3.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/agent.js CHANGED
@@ -3,7 +3,7 @@ const os = require('os')
3
3
  const path = require('path')
4
4
  const chalk = require('chalk')
5
5
  const { getConfig } = require('./config')
6
- const { readFile, writeFile, editFile, listFiles, runCommand, searchCode } = require('./tools')
6
+ const { readFile, writeFile, editFile, listFiles, runCommand, searchCode, openApp, openUrl, getSystemInfo, takeScreenshot } = require('./tools')
7
7
  const { chatCompletion } = require('./api')
8
8
 
9
9
  class OtoroAgent {
@@ -117,6 +117,21 @@ class OtoroAgent {
117
117
  console.log(chalk.yellow(` ⚡ Running: ${payload.cmd}`))
118
118
  result = runCommand(payload.cmd, payload.timeout || 30000)
119
119
  break
120
+ case 'open_app':
121
+ console.log(chalk.yellow(` 🚀 Opening: ${payload.app}`))
122
+ result = openApp(payload.app)
123
+ break
124
+ case 'open_url':
125
+ console.log(chalk.yellow(` 🌐 Opening: ${payload.url}`))
126
+ result = openUrl(payload.url)
127
+ break
128
+ case 'screenshot':
129
+ console.log(chalk.yellow(` 📸 Taking screenshot...`))
130
+ result = takeScreenshot()
131
+ break
132
+ case 'system_info':
133
+ result = getSystemInfo()
134
+ break
120
135
  case 'list_files':
121
136
  result = listFiles(payload.dir || '.', payload.pattern || '')
122
137
  break
@@ -158,9 +173,15 @@ Available tools (use XML tags in your response):
158
173
  - <tool:read path="file"/> — read a file
159
174
  - <tool:write path="file">content</tool:write> — write/create a file
160
175
  - <tool:edit path="file" old="old text" new="new text"/> — edit a file
161
- - <tool:run cmd="command"/> — run a shell command
176
+ - <tool:run cmd="command"/> — run a shell command (works on Mac/Windows/Linux)
162
177
  - <tool:search pattern="regex"/> — search code
178
+ - <tool:open app="AppName"/> — open an application (Blender, Discord, VS Code, etc.)
179
+ - <tool:url href="https://..."/> — open a URL in the browser
180
+ - <tool:screenshot/> — take a screenshot of the screen
181
+
182
+ You are running LOCALLY on the user's machine. You have full access to their filesystem and can run any command. You can open apps, edit code, run builds, start servers, and interact with their development environment.
163
183
 
184
+ Platform: ${process.platform} (${process.arch})
164
185
  Project context:
165
186
  ${projectContext}
166
187
 
@@ -238,6 +259,27 @@ Be direct. Execute tasks immediately. Don't ask for permission — just do it.`
238
259
  results.push({ type: 'search', pattern: match[1], ...r })
239
260
  }
240
261
 
262
+ // Process <tool:open>
263
+ for (const match of response.matchAll(/<tool:open\s+app="([^"]+)"\s*\/>/g)) {
264
+ console.log(chalk.yellow(` 🚀 Opening: ${match[1]}`))
265
+ const r = openApp(match[1])
266
+ results.push({ type: 'open_app', app: match[1], ...r })
267
+ }
268
+
269
+ // Process <tool:url>
270
+ for (const match of response.matchAll(/<tool:url\s+href="([^"]+)"\s*\/>/g)) {
271
+ console.log(chalk.yellow(` 🌐 Opening: ${match[1]}`))
272
+ const r = openUrl(match[1])
273
+ results.push({ type: 'open_url', url: match[1], ...r })
274
+ }
275
+
276
+ // Process <tool:screenshot>
277
+ for (const match of response.matchAll(/<tool:screenshot\s*\/>/g)) {
278
+ console.log(chalk.yellow(` 📸 Screenshot...`))
279
+ const r = takeScreenshot()
280
+ results.push({ type: 'screenshot', ...r })
281
+ }
282
+
241
283
  return results
242
284
  }
243
285
  }
package/lib/config.js CHANGED
@@ -6,10 +6,39 @@ const readline = require('readline')
6
6
  const https = require('https')
7
7
  const http = require('http')
8
8
 
9
+ const crypto = require('crypto')
10
+
9
11
  const CONFIG_DIR = path.join(os.homedir(), '.otoro')
10
12
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
11
13
  const PROJECT_FILE = '.otoro.json'
12
14
 
15
+ // Encryption for API key storage — key derived from machine ID
16
+ const ENCRYPTION_KEY = crypto
17
+ .createHash('sha256')
18
+ .update(os.hostname() + os.userInfo().username + '__otoro_seal__')
19
+ .digest()
20
+
21
+ function encrypt(text) {
22
+ const iv = crypto.randomBytes(16)
23
+ const cipher = crypto.createCipheriv('aes-256-cbc', ENCRYPTION_KEY, iv)
24
+ let encrypted = cipher.update(text, 'utf8', 'hex')
25
+ encrypted += cipher.final('hex')
26
+ return iv.toString('hex') + ':' + encrypted
27
+ }
28
+
29
+ function decrypt(data) {
30
+ try {
31
+ const [ivHex, encrypted] = data.split(':')
32
+ const iv = Buffer.from(ivHex, 'hex')
33
+ const decipher = crypto.createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, iv)
34
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8')
35
+ decrypted += decipher.final('utf8')
36
+ return decrypted
37
+ } catch {
38
+ return '' // corrupted or wrong machine
39
+ }
40
+ }
41
+
13
42
  const DEFAULT_CONFIG = {
14
43
  api_url: 'https://otoroagi.com/api/otoro',
15
44
  gpu_url: '',
@@ -23,7 +52,13 @@ const DEFAULT_CONFIG = {
23
52
  function getConfig() {
24
53
  try {
25
54
  if (fs.existsSync(CONFIG_FILE)) {
26
- return { ...DEFAULT_CONFIG, ...JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) }
55
+ const raw = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'))
56
+ // Decrypt API key if encrypted
57
+ if (raw._api_key_enc) {
58
+ raw.api_key = decrypt(raw._api_key_enc)
59
+ delete raw._api_key_enc
60
+ }
61
+ return { ...DEFAULT_CONFIG, ...raw }
27
62
  }
28
63
  } catch {}
29
64
  return DEFAULT_CONFIG
@@ -31,7 +66,13 @@ function getConfig() {
31
66
 
32
67
  function saveConfig(config) {
33
68
  fs.mkdirSync(CONFIG_DIR, { recursive: true })
34
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 })
69
+ // Encrypt the API key before saving
70
+ const toSave = { ...config }
71
+ if (toSave.api_key) {
72
+ toSave._api_key_enc = encrypt(toSave.api_key)
73
+ delete toSave.api_key
74
+ }
75
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(toSave, null, 2), { mode: 0o600 })
35
76
  }
36
77
 
37
78
  function isLoggedIn() {
package/lib/tools.js CHANGED
@@ -102,4 +102,72 @@ function processToolCalls(response) {
102
102
  return actions
103
103
  }
104
104
 
105
- module.exports = { readFile, writeFile, editFile, listFiles, runCommand, searchCode, processToolCalls }
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()} &`
117
+ }
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
+ }
124
+
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
+ }
137
+ }
138
+
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
+ }
153
+ }
154
+
155
+ function takeScreenshot() {
156
+ const platform = process.platform
157
+ const tmpFile = path.join(require('os').tmpdir(), `otoro-screenshot-${Date.now()}.png`)
158
+ 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' }
168
+ } catch (e) {
169
+ return { success: false, error: e.message }
170
+ }
171
+ }
172
+
173
+ module.exports = { readFile, writeFile, editFile, listFiles, runCommand, searchCode, processToolCalls, openApp, openUrl, getSystemInfo, takeScreenshot }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "otoro-cli",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Otoro AGI — AI coding assistant for your terminal. Code, generate images, execute tasks, and control your projects remotely.",
5
5
  "main": "index.js",
6
6
  "bin": {