otoro-cli 1.0.0 → 1.2.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.
Files changed (3) hide show
  1. package/bin/otoro.js +39 -8
  2. package/lib/config.js +156 -14
  3. package/package.json +1 -1
package/bin/otoro.js CHANGED
@@ -1,8 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  const { program } = require('commander')
3
- const { startChat } = require('../lib/chat')
4
- const { runTask } = require('../lib/task')
5
- const { initConfig, getConfig } = require('../lib/config')
3
+ const { initConfig, login, logout, isLoggedIn, requireAuth } = require('../lib/config')
6
4
  const chalk = require('chalk')
7
5
  const pkg = require('../package.json')
8
6
 
@@ -11,12 +9,26 @@ program
11
9
  .description('Otoro AGI — AI coding assistant')
12
10
  .version(pkg.version)
13
11
 
12
+ program
13
+ .command('login')
14
+ .description('Sign in with your Otoro account')
15
+ .action(async () => {
16
+ await login()
17
+ })
18
+
19
+ program
20
+ .command('logout')
21
+ .description('Sign out and clear credentials')
22
+ .action(async () => {
23
+ await logout()
24
+ })
25
+
14
26
  program
15
27
  .command('init')
16
28
  .description('Initialize Otoro in this project')
17
29
  .action(async () => {
18
30
  await initConfig()
19
- console.log(chalk.green('✓ Otoro initialized. Run `otoro` to start coding.'))
31
+ console.log(chalk.green(' Run `otoro` to start coding.\n'))
20
32
  })
21
33
 
22
34
  program
@@ -24,6 +36,8 @@ program
24
36
  .alias('c')
25
37
  .description('Start interactive chat')
26
38
  .action(async () => {
39
+ requireAuth()
40
+ const { startChat } = require('../lib/chat')
27
41
  await startChat()
28
42
  })
29
43
 
@@ -31,6 +45,7 @@ program
31
45
  .command('ask <prompt...>')
32
46
  .description('Ask Otoro a single question')
33
47
  .action(async (prompt) => {
48
+ requireAuth()
34
49
  const { askOnce } = require('../lib/chat')
35
50
  await askOnce(prompt.join(' '))
36
51
  })
@@ -39,6 +54,8 @@ program
39
54
  .command('run <task...>')
40
55
  .description('Run a coding task (write + execute + verify)')
41
56
  .action(async (task) => {
57
+ requireAuth()
58
+ const { runTask } = require('../lib/task')
42
59
  await runTask(task.join(' '))
43
60
  })
44
61
 
@@ -46,14 +63,16 @@ program
46
63
  .command('image <prompt...>')
47
64
  .description('Generate an image')
48
65
  .action(async (prompt) => {
66
+ requireAuth()
49
67
  const { generateImage } = require('../lib/image')
50
68
  await generateImage(prompt.join(' '))
51
69
  })
52
70
 
53
71
  program
54
72
  .command('start')
55
- .description('Start Otoro agent daemon — connects to server, waits for remote tasks')
73
+ .description('Start Otoro agent daemon — connects to server for remote tasks')
56
74
  .action(async () => {
75
+ requireAuth()
57
76
  const { startAgent } = require('../lib/agent')
58
77
  await startAgent()
59
78
  })
@@ -62,8 +81,9 @@ program
62
81
  .command('status')
63
82
  .description('Check connected agents and server status')
64
83
  .action(async () => {
84
+ requireAuth()
85
+ const config = require('../lib/config').getConfig()
65
86
  const http = require('http')
66
- const config = getConfig()
67
87
  const url = `${config.gpu_url}/v1/agents`
68
88
  http.get(url, { headers: { 'Authorization': `Bearer ${config.api_key}` } }, (res) => {
69
89
  let data = ''
@@ -85,9 +105,20 @@ program
85
105
  }).on('error', (e) => console.log(chalk.red(` Error: ${e.message}`)))
86
106
  })
87
107
 
88
- // Default: start interactive chat
108
+ // Default: no args → check login then start chat
89
109
  if (process.argv.length <= 2) {
90
- startChat()
110
+ if (!isLoggedIn()) {
111
+ console.log(chalk.cyan.bold('\n 🐙 Otoro AGI\n'))
112
+ console.log(chalk.yellow(' Not logged in. Run:\n'))
113
+ console.log(chalk.white(' otoro login — Sign in with your Otoro account'))
114
+ console.log(chalk.white(' otoro init — Initialize a project'))
115
+ console.log(chalk.white(' otoro — Start coding\n'))
116
+ console.log(chalk.gray(' Don\'t have an account? Sign up at https://otoroagi.com\n'))
117
+ } else {
118
+ requireAuth()
119
+ const { startChat } = require('../lib/chat')
120
+ startChat()
121
+ }
91
122
  } else {
92
123
  program.parse()
93
124
  }
package/lib/config.js CHANGED
@@ -3,15 +3,48 @@ const path = require('path')
3
3
  const os = require('os')
4
4
  const chalk = require('chalk')
5
5
  const readline = require('readline')
6
+ const https = require('https')
7
+ const http = require('http')
8
+
9
+ const crypto = require('crypto')
6
10
 
7
11
  const CONFIG_DIR = path.join(os.homedir(), '.otoro')
8
12
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
9
13
  const PROJECT_FILE = '.otoro.json'
10
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
+
11
42
  const DEFAULT_CONFIG = {
12
43
  api_url: 'https://otoroagi.com/api/otoro',
13
- gpu_url: 'http://100.124.135.21:8000',
14
- api_key: 'otoro-secret',
44
+ gpu_url: '',
45
+ api_key: '',
46
+ user_id: '',
47
+ email: '',
15
48
  model: 'qwen-coder',
16
49
  theme: 'dark',
17
50
  }
@@ -19,7 +52,13 @@ const DEFAULT_CONFIG = {
19
52
  function getConfig() {
20
53
  try {
21
54
  if (fs.existsSync(CONFIG_FILE)) {
22
- 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 }
23
62
  }
24
63
  } catch {}
25
64
  return DEFAULT_CONFIG
@@ -27,28 +66,131 @@ function getConfig() {
27
66
 
28
67
  function saveConfig(config) {
29
68
  fs.mkdirSync(CONFIG_DIR, { recursive: true })
30
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
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 })
31
76
  }
32
77
 
33
- async function initConfig() {
78
+ function isLoggedIn() {
34
79
  const config = getConfig()
80
+ return !!(config.api_key && config.user_id)
81
+ }
82
+
83
+ function requireAuth() {
84
+ if (!isLoggedIn()) {
85
+ console.log(chalk.red('\n ✗ Not logged in. Run `otoro login` first.\n'))
86
+ process.exit(1)
87
+ }
88
+ return getConfig()
89
+ }
90
+
91
+ async function login() {
35
92
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
36
93
  const ask = (q) => new Promise(r => rl.question(q, r))
37
94
 
38
- console.log(chalk.cyan.bold('\n 🐙 Otoro AGI Setup\n'))
95
+ console.log(chalk.cyan.bold('\n 🐙 Otoro AGI — Login\n'))
96
+ console.log(chalk.gray(' Sign in with your Otoro account (otoroagi.com)\n'))
97
+
98
+ const email = await ask(chalk.white(' Email: '))
99
+ const password = await ask(chalk.white(' Password: '))
100
+
101
+ if (!email.trim() || !password.trim()) {
102
+ console.log(chalk.red('\n ✗ Email and password are required.\n'))
103
+ rl.close()
104
+ return
105
+ }
106
+
107
+ console.log(chalk.gray('\n Authenticating...'))
108
+
109
+ try {
110
+ // Authenticate with Supabase via the website
111
+ const result = await new Promise((resolve, reject) => {
112
+ const body = JSON.stringify({ email: email.trim(), password: password.trim() })
113
+ const req = https.request('https://otoroagi.com/api/otoro/auth', {
114
+ method: 'POST',
115
+ headers: { 'Content-Type': 'application/json' },
116
+ timeout: 15000,
117
+ }, (res) => {
118
+ let data = ''
119
+ res.on('data', c => data += c)
120
+ res.on('end', () => {
121
+ try { resolve(JSON.parse(data)) }
122
+ catch { reject(new Error('Invalid response from server')) }
123
+ })
124
+ })
125
+ req.on('error', reject)
126
+ req.on('timeout', () => { req.destroy(); reject(new Error('Connection timed out')) })
127
+ req.write(body)
128
+ req.end()
129
+ })
130
+
131
+ if (result.error) {
132
+ console.log(chalk.red(`\n ✗ ${result.error}\n`))
133
+ rl.close()
134
+ return
135
+ }
136
+
137
+ if (result.success && result.api_key) {
138
+ const config = getConfig()
139
+ config.api_key = result.api_key
140
+ config.user_id = result.user_id
141
+ config.email = email.trim()
142
+ config.gpu_url = result.gpu_url || config.gpu_url
143
+ saveConfig(config)
144
+
145
+ console.log(chalk.green(`\n ✓ Logged in as ${email.trim()}`))
146
+ console.log(chalk.gray(` Config saved to ${CONFIG_FILE}\n`))
147
+ console.log(chalk.cyan(' Get started:'))
148
+ console.log(chalk.gray(' otoro — Interactive chat'))
149
+ console.log(chalk.gray(' otoro start — Agent daemon (remote tasks)'))
150
+ console.log(chalk.gray(' otoro ask "?" — Quick question\n'))
151
+ } else {
152
+ console.log(chalk.red('\n ✗ Login failed. Check your credentials.\n'))
153
+ }
154
+ } catch (e) {
155
+ console.log(chalk.red(`\n ✗ ${e.message}\n`))
156
+ }
157
+
158
+ rl.close()
159
+ }
160
+
161
+ async function logout() {
162
+ if (fs.existsSync(CONFIG_FILE)) {
163
+ const config = getConfig()
164
+ config.api_key = ''
165
+ config.user_id = ''
166
+ config.email = ''
167
+ saveConfig(config)
168
+ console.log(chalk.green('\n ✓ Logged out. Credentials cleared.\n'))
169
+ } else {
170
+ console.log(chalk.gray('\n Already logged out.\n'))
171
+ }
172
+ }
173
+
174
+ async function initConfig() {
175
+ const config = requireAuth()
176
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
177
+ const ask = (q) => new Promise(r => rl.question(q, r))
39
178
 
40
- const url = await ask(chalk.gray(` GPU URL [${config.gpu_url}]: `))
41
- if (url.trim()) config.gpu_url = url.trim()
179
+ console.log(chalk.cyan.bold('\n 🐙 Otoro AGI — Project Setup\n'))
180
+ console.log(chalk.gray(` Logged in as: ${config.email}`))
181
+ console.log(chalk.gray(` Directory: ${process.cwd()}\n`))
42
182
 
43
- const key = await ask(chalk.gray(` API Key [${config.api_key}]: `))
44
- if (key.trim()) config.api_key = key.trim()
183
+ const name = await ask(chalk.gray(` Project name [${path.basename(process.cwd())}]: `))
45
184
 
46
185
  rl.close()
47
- saveConfig(config)
48
186
 
49
- // Create project config
50
- const projectConfig = { name: path.basename(process.cwd()), created: new Date().toISOString() }
187
+ const projectConfig = {
188
+ name: name.trim() || path.basename(process.cwd()),
189
+ created: new Date().toISOString(),
190
+ user_id: config.user_id,
191
+ }
51
192
  fs.writeFileSync(PROJECT_FILE, JSON.stringify(projectConfig, null, 2))
193
+ console.log(chalk.green(`\n ✓ Project "${projectConfig.name}" initialized.\n`))
52
194
  }
53
195
 
54
- module.exports = { getConfig, saveConfig, initConfig }
196
+ module.exports = { getConfig, saveConfig, initConfig, login, logout, isLoggedIn, requireAuth }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "otoro-cli",
3
- "version": "1.0.0",
3
+ "version": "1.2.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": {