omnibiofex 2.4.1 β 2.4.2
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/obx +6 -0
- package/package.json +3 -15
- package/src/api.js +11 -6
- package/src/auth.js +118 -18
- package/src/commands/debug.js +37 -0
- package/src/commands/research.js +102 -17
- package/src/config.js +3 -0
- package/src/utils/display.js +226 -0
package/bin/obx
CHANGED
|
@@ -6,10 +6,16 @@ const { login, logout } = require('../src/auth');
|
|
|
6
6
|
const { missionCreate, missionStatus, missionResults, missionList } = require('../src/commands/mission');
|
|
7
7
|
const { literatureReview, gaps, hypothesis } = require('../src/commands/research');
|
|
8
8
|
const { credits, usage, buy } = require('../src/commands/account');
|
|
9
|
+
const { debug } = require('../src/commands/debug');
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
console.log(chalk.hex('#F24E1E')(figlet.textSync('OmniBioFex X', { horizontalLayout: 'full' })));
|
|
11
13
|
console.log(chalk.gray('The Autonomous Research Terminal v2.4.1\n'));
|
|
12
14
|
|
|
15
|
+
program
|
|
16
|
+
.command('debug')
|
|
17
|
+
.description('Debug authentication and token status')
|
|
18
|
+
.action(debug);
|
|
13
19
|
program
|
|
14
20
|
.command('login').description('Authenticate').action(login);
|
|
15
21
|
program
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omnibiofex",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"description": "OmniBioFex X - The Autonomous Research Terminal for AI-powered research missions",
|
|
5
5
|
"main": "bin/obx",
|
|
6
6
|
"bin": {
|
|
7
|
-
"obx": "bin/obx"
|
|
7
|
+
"obx": "./bin/obx"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/obx",
|
|
@@ -16,12 +16,7 @@
|
|
|
16
16
|
"terminal",
|
|
17
17
|
"cli",
|
|
18
18
|
"autonomous",
|
|
19
|
-
"omnibiofex"
|
|
20
|
-
"academic",
|
|
21
|
-
"scientific",
|
|
22
|
-
"literature-review",
|
|
23
|
-
"research-agent",
|
|
24
|
-
"multi-agent"
|
|
19
|
+
"omnibiofex"
|
|
25
20
|
],
|
|
26
21
|
"author": {
|
|
27
22
|
"name": "OmniBioFex",
|
|
@@ -30,13 +25,6 @@
|
|
|
30
25
|
},
|
|
31
26
|
"license": "MIT",
|
|
32
27
|
"homepage": "https://x.omnibiofex.cloud",
|
|
33
|
-
"repository": {
|
|
34
|
-
"type": "git",
|
|
35
|
-
"url": "git+https://github.com/omnibiofex/omnibiofex-cli.git"
|
|
36
|
-
},
|
|
37
|
-
"bugs": {
|
|
38
|
-
"url": "https://github.com/omnibiofex/omnibiofex-cli/issues"
|
|
39
|
-
},
|
|
40
28
|
"engines": {
|
|
41
29
|
"node": ">=14.0.0"
|
|
42
30
|
},
|
package/src/api.js
CHANGED
|
@@ -7,11 +7,16 @@ const apiClient = axios.create({
|
|
|
7
7
|
timeout: 540000, // 9 minutes
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
apiClient.interceptors.request.use((requestConfig) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
// π₯ FIX: Make the interceptor async to handle async getAuthToken()
|
|
11
|
+
apiClient.interceptors.request.use(async (requestConfig) => {
|
|
12
|
+
try {
|
|
13
|
+
const token = await getAuthToken(); // π₯ Now properly awaits the token
|
|
14
|
+
requestConfig.headers.Authorization = `Bearer ${token}`;
|
|
15
|
+
return requestConfig;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error(chalk.red('Failed to get auth token:'), error.message);
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
15
20
|
});
|
|
16
21
|
|
|
17
22
|
// Handle errors
|
|
@@ -55,7 +60,7 @@ async function getUserCredits() {
|
|
|
55
60
|
const response = await apiClient.get('https://getusercredits-yyedhmslhq-uc.a.run.app');
|
|
56
61
|
return {
|
|
57
62
|
balance: response.data.tokens || 0,
|
|
58
|
-
used: 0,
|
|
63
|
+
used: 0,
|
|
59
64
|
total: response.data.tokens || 0
|
|
60
65
|
};
|
|
61
66
|
} catch (error) {
|
package/src/auth.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const http = require('http');
|
|
2
2
|
const { URL } = require('url');
|
|
3
|
+
const axios = require('axios');
|
|
3
4
|
const chalk = require('chalk');
|
|
4
5
|
const inquirer = require('inquirer');
|
|
5
6
|
const { initializeApp } = require('firebase/app');
|
|
@@ -36,6 +37,10 @@ async function login() {
|
|
|
36
37
|
console.log(chalk.gray('Login cancelled.'));
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
40
|
+
// Clear old tokens
|
|
41
|
+
config.delete('authToken');
|
|
42
|
+
config.delete('refreshToken');
|
|
43
|
+
config.delete('tokenExpiry');
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
const { email } = await inquirer.prompt([{
|
|
@@ -86,13 +91,22 @@ async function login() {
|
|
|
86
91
|
process.exit(1);
|
|
87
92
|
}, 5 * 60 * 1000);
|
|
88
93
|
|
|
89
|
-
const
|
|
94
|
+
const authData = await waitForAuth();
|
|
90
95
|
|
|
91
96
|
cleanup();
|
|
92
97
|
|
|
93
|
-
if (token) {
|
|
94
|
-
|
|
98
|
+
if (authData && authData.token) {
|
|
99
|
+
// π₯ Validate token format before storing
|
|
100
|
+
if (!authData.token.startsWith('eyJ')) {
|
|
101
|
+
throw new Error('Invalid token format received');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
config.set('authToken', authData.token);
|
|
105
|
+
if (authData.refreshToken) {
|
|
106
|
+
config.set('refreshToken', authData.refreshToken);
|
|
107
|
+
}
|
|
95
108
|
config.set('userEmail', email);
|
|
109
|
+
config.set('tokenExpiry', Date.now() + (55 * 60 * 1000)); // 55 minutes
|
|
96
110
|
config.delete('pendingLogin');
|
|
97
111
|
config.delete('pendingEmail');
|
|
98
112
|
|
|
@@ -100,10 +114,9 @@ async function login() {
|
|
|
100
114
|
console.log(chalk.hex('#F24E1E')(`\nπ Welcome to OmniBioFex X, ${email}!`));
|
|
101
115
|
console.log(chalk.gray('You can now use all CLI commands.\n'));
|
|
102
116
|
|
|
103
|
-
// π₯ THE FIX: Exit the process cleanly
|
|
104
117
|
process.exit(0);
|
|
105
118
|
} else {
|
|
106
|
-
throw new Error('No
|
|
119
|
+
throw new Error('No authentication data received');
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
} catch (error) {
|
|
@@ -135,7 +148,6 @@ function startLocalServer() {
|
|
|
135
148
|
const server = http.createServer((req, res) => {
|
|
136
149
|
const url = new URL(req.url, `http://localhost:${LOCAL_PORT}`);
|
|
137
150
|
|
|
138
|
-
// Ignore favicon requests
|
|
139
151
|
if (url.pathname === '/favicon.ico') {
|
|
140
152
|
res.writeHead(204);
|
|
141
153
|
res.end();
|
|
@@ -145,11 +157,35 @@ function startLocalServer() {
|
|
|
145
157
|
console.log('Local server received request:', req.url);
|
|
146
158
|
|
|
147
159
|
if (url.pathname === CALLBACK_PATH) {
|
|
148
|
-
|
|
160
|
+
// π₯ FIX: Properly decode URL parameters
|
|
161
|
+
let token = url.searchParams.get('token');
|
|
162
|
+
let refreshToken = url.searchParams.get('refreshToken');
|
|
149
163
|
const error = url.searchParams.get('error');
|
|
150
164
|
|
|
165
|
+
// π₯ FIX: Replace spaces with + (URL decoding issue)
|
|
166
|
+
if (token && token.includes(' ')) {
|
|
167
|
+
console.log(chalk.yellow('β οΈ Token contains spaces, fixing...'));
|
|
168
|
+
token = token.replace(/ /g, '+');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (refreshToken && refreshToken.includes(' ')) {
|
|
172
|
+
refreshToken = refreshToken.replace(/ /g, '+');
|
|
173
|
+
}
|
|
174
|
+
|
|
151
175
|
if (token) {
|
|
152
176
|
console.log('β Received auth token from browser');
|
|
177
|
+
console.log('Token length:', token.length);
|
|
178
|
+
console.log('Token starts with:', token.substring(0, 20));
|
|
179
|
+
|
|
180
|
+
// π₯ Validate token format
|
|
181
|
+
if (!token.startsWith('eyJ')) {
|
|
182
|
+
console.error('β Invalid token format');
|
|
183
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
184
|
+
res.end('<h1>Invalid Token</h1><p>The authentication token is malformed.</p>');
|
|
185
|
+
global._authData = null;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
153
189
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
154
190
|
res.end(`
|
|
155
191
|
<!DOCTYPE html>
|
|
@@ -169,7 +205,6 @@ function startLocalServer() {
|
|
|
169
205
|
justify-content: center; margin: 0 auto 24px; }
|
|
170
206
|
h1 { margin: 0 0 12px; font-size: 24px; }
|
|
171
207
|
p { color: #737373; margin: 0 0 24px; }
|
|
172
|
-
.close { color: #F24E1E; font-size: 14px; font-family: monospace; }
|
|
173
208
|
</style>
|
|
174
209
|
</head>
|
|
175
210
|
<body>
|
|
@@ -177,13 +212,12 @@ function startLocalServer() {
|
|
|
177
212
|
<div class="check">β</div>
|
|
178
213
|
<h1>Authentication Successful!</h1>
|
|
179
214
|
<p>You can close this window and return to your terminal.</p>
|
|
180
|
-
<div class="close">You may close this tab now.</div>
|
|
181
215
|
</div>
|
|
182
216
|
</body>
|
|
183
217
|
</html>
|
|
184
218
|
`);
|
|
185
219
|
|
|
186
|
-
global.
|
|
220
|
+
global._authData = { token, refreshToken };
|
|
187
221
|
} else {
|
|
188
222
|
console.error('β No token received, error:', error);
|
|
189
223
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
@@ -203,12 +237,11 @@ function startLocalServer() {
|
|
|
203
237
|
<div class="card">
|
|
204
238
|
<h1>Authentication Failed</h1>
|
|
205
239
|
<p>${error || 'Unknown error'}</p>
|
|
206
|
-
<p>Please close this window and try again in your terminal.</p>
|
|
207
240
|
</div>
|
|
208
241
|
</body>
|
|
209
242
|
</html>
|
|
210
243
|
`);
|
|
211
|
-
global.
|
|
244
|
+
global._authData = null;
|
|
212
245
|
}
|
|
213
246
|
} else {
|
|
214
247
|
res.writeHead(404);
|
|
@@ -226,20 +259,64 @@ function startLocalServer() {
|
|
|
226
259
|
function waitForAuth() {
|
|
227
260
|
return new Promise((resolve) => {
|
|
228
261
|
const checkInterval = setInterval(() => {
|
|
229
|
-
if (global.
|
|
262
|
+
if (global._authData !== undefined) {
|
|
230
263
|
clearInterval(checkInterval);
|
|
231
|
-
const
|
|
232
|
-
delete global.
|
|
233
|
-
resolve(
|
|
264
|
+
const authData = global._authData;
|
|
265
|
+
delete global._authData;
|
|
266
|
+
resolve(authData);
|
|
234
267
|
}
|
|
235
268
|
}, 500);
|
|
236
269
|
});
|
|
237
270
|
}
|
|
238
271
|
|
|
272
|
+
async function refreshAuthToken() {
|
|
273
|
+
const refreshToken = config.get('refreshToken');
|
|
274
|
+
|
|
275
|
+
if (!refreshToken) {
|
|
276
|
+
throw new Error('No refresh token available. Please login again.');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
console.log(chalk.gray('π Refreshing authentication token...'));
|
|
281
|
+
|
|
282
|
+
const response = await axios.post(
|
|
283
|
+
`https://securetoken.googleapis.com/v1/token?key=${firebaseConfig.apiKey}`,
|
|
284
|
+
new URLSearchParams({
|
|
285
|
+
grant_type: 'refresh_token',
|
|
286
|
+
refresh_token: refreshToken
|
|
287
|
+
}).toString(),
|
|
288
|
+
{
|
|
289
|
+
headers: {
|
|
290
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const { id_token, refresh_token, expires_in } = response.data;
|
|
296
|
+
|
|
297
|
+
// π₯ Validate new token
|
|
298
|
+
if (!id_token || !id_token.startsWith('eyJ')) {
|
|
299
|
+
throw new Error('Invalid token received from refresh endpoint');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
config.set('authToken', id_token);
|
|
303
|
+
config.set('refreshToken', refresh_token);
|
|
304
|
+
config.set('tokenExpiry', Date.now() + ((expires_in - 300) * 1000));
|
|
305
|
+
|
|
306
|
+
console.log(chalk.green('β Token refreshed successfully!'));
|
|
307
|
+
return id_token;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
console.error(chalk.red('β Failed to refresh token:', error.message));
|
|
310
|
+
throw new Error('Token refresh failed. Please login again.');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
239
314
|
function logout() {
|
|
240
315
|
config.delete('authToken');
|
|
316
|
+
config.delete('refreshToken');
|
|
241
317
|
config.delete('userId');
|
|
242
318
|
config.delete('userEmail');
|
|
319
|
+
config.delete('tokenExpiry');
|
|
243
320
|
console.log(chalk.green('β Logged out successfully'));
|
|
244
321
|
process.exit(0);
|
|
245
322
|
}
|
|
@@ -248,13 +325,36 @@ function isAuthenticated() {
|
|
|
248
325
|
return config.get('authToken') !== null;
|
|
249
326
|
}
|
|
250
327
|
|
|
251
|
-
function
|
|
328
|
+
function isTokenExpired() {
|
|
329
|
+
const expiry = config.get('tokenExpiry');
|
|
330
|
+
if (!expiry) return true;
|
|
331
|
+
return Date.now() >= expiry;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function getAuthToken() {
|
|
335
|
+
if (isTokenExpired()) {
|
|
336
|
+
try {
|
|
337
|
+
await refreshAuthToken();
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.error(chalk.red(error.message));
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
252
344
|
const token = config.get('authToken');
|
|
253
345
|
if (!token) {
|
|
254
346
|
console.error(chalk.red('Not authenticated. Please run: obx login'));
|
|
255
347
|
process.exit(1);
|
|
256
348
|
}
|
|
349
|
+
|
|
350
|
+
// π₯ Validate token before returning
|
|
351
|
+
if (!token.startsWith('eyJ')) {
|
|
352
|
+
console.error(chalk.red('Stored token is corrupted. Please login again.'));
|
|
353
|
+
config.delete('authToken');
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
|
|
257
357
|
return token;
|
|
258
358
|
}
|
|
259
359
|
|
|
260
|
-
module.exports = { login, logout, isAuthenticated, getAuthToken };
|
|
360
|
+
module.exports = { login, logout, isAuthenticated, getAuthToken, refreshAuthToken };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const config = require('../config');
|
|
3
|
+
const { isAuthenticated } = require('../auth');
|
|
4
|
+
|
|
5
|
+
async function debug() {
|
|
6
|
+
console.log(chalk.hex('#F24E1E')('\nπ Debug Information\n'));
|
|
7
|
+
|
|
8
|
+
console.log(chalk.white('Authentication Status:'), isAuthenticated() ? chalk.green('β Logged in') : chalk.red('β Not logged in'));
|
|
9
|
+
console.log(chalk.white('User Email:'), config.get('userEmail') || 'Not set');
|
|
10
|
+
console.log(chalk.white('Token Expiry:'), config.get('tokenExpiry') ? new Date(config.get('tokenExpiry')).toLocaleString() : 'Not set');
|
|
11
|
+
|
|
12
|
+
const token = config.get('authToken');
|
|
13
|
+
if (token) {
|
|
14
|
+
console.log(chalk.white('\nToken Info:'));
|
|
15
|
+
console.log(chalk.gray(' Length:'), token.length, 'characters');
|
|
16
|
+
console.log(chalk.gray(' Starts with:'), token.substring(0, 20));
|
|
17
|
+
console.log(chalk.gray(' Contains spaces:'), token.includes(' ') ? chalk.red('YES (CORRUPTED!)') : chalk.green('No'));
|
|
18
|
+
console.log(chalk.gray(' Contains +:'), token.includes('+') ? chalk.yellow('Yes') : chalk.green('No'));
|
|
19
|
+
console.log(chalk.gray(' Valid JWT format:'), token.startsWith('eyJ') ? chalk.green('Yes') : chalk.red('No'));
|
|
20
|
+
|
|
21
|
+
// Check for corruption
|
|
22
|
+
if (token.includes(' ')) {
|
|
23
|
+
console.log(chalk.red('\nβ οΈ TOKEN IS CORRUPTED! Contains spaces.'));
|
|
24
|
+
console.log(chalk.gray('This happens when + characters in JWT are converted to spaces during URL handling.'));
|
|
25
|
+
console.log(chalk.gray('Please login again with the fixed version.'));
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
console.log(chalk.yellow('\nNo token stored'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(chalk.white('\nConfig File Location:'));
|
|
32
|
+
console.log(chalk.gray(' Windows: %APPDATA%\\omnibiofex\\config.json'));
|
|
33
|
+
console.log(chalk.gray(' macOS: ~/Library/Preferences/omnibiofex-nodejs/config.json'));
|
|
34
|
+
console.log(chalk.gray(' Linux: ~/.config/omnibiofex-nodejs/config.json'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { debug };
|
package/src/commands/research.js
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
const ora = require('ora');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
3
6
|
const { createMission } = require('../api');
|
|
4
7
|
const { isAuthenticated } = require('../auth');
|
|
8
|
+
const { showThinking, displayReport, renderMarkdown } = require('../utils/display');
|
|
9
|
+
|
|
10
|
+
// Create reports directory
|
|
11
|
+
const REPORTS_DIR = path.join(os.homedir(), 'obx-reports');
|
|
12
|
+
if (!fs.existsSync(REPORTS_DIR)) {
|
|
13
|
+
fs.mkdirSync(REPORTS_DIR, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function saveReport(topic, content) {
|
|
17
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
18
|
+
const sanitizedTopic = topic.replace(/[^a-z0-9]/gi, '_').substring(0, 50);
|
|
19
|
+
const filename = `${sanitizedTopic}_${timestamp}.md`;
|
|
20
|
+
const filepath = path.join(REPORTS_DIR, filename);
|
|
21
|
+
|
|
22
|
+
fs.writeFileSync(filepath, content, 'utf8');
|
|
23
|
+
return filepath;
|
|
24
|
+
}
|
|
5
25
|
|
|
6
26
|
async function literatureReview(topic) {
|
|
7
27
|
if (!isAuthenticated()) {
|
|
@@ -9,15 +29,29 @@ async function literatureReview(topic) {
|
|
|
9
29
|
return;
|
|
10
30
|
}
|
|
11
31
|
|
|
32
|
+
// π₯ Show thinking process
|
|
33
|
+
await showThinking('LITERATURE_REVIEW', topic);
|
|
34
|
+
|
|
12
35
|
const spinner = ora(`Generating literature review for: ${topic}`).start();
|
|
13
36
|
|
|
14
37
|
try {
|
|
15
|
-
const result = await createMission(topic, '
|
|
38
|
+
const result = await createMission(topic, 'LITERATURE_REVIEW');
|
|
16
39
|
|
|
17
40
|
spinner.succeed(chalk.green('Literature review complete!'));
|
|
18
|
-
|
|
19
|
-
console.log(chalk.gray(
|
|
20
|
-
console.log(chalk.
|
|
41
|
+
|
|
42
|
+
console.log(chalk.gray(`\nπ Model: ${result.model}`));
|
|
43
|
+
console.log(chalk.gray(`π° RCC Cost: ${result.rccCost}`));
|
|
44
|
+
console.log(chalk.gray(`π³ Remaining Balance: ${result.rccBalance} RCC`));
|
|
45
|
+
|
|
46
|
+
// π₯ Display the report with typing effect
|
|
47
|
+
await displayReport(result.response, true);
|
|
48
|
+
|
|
49
|
+
console.log(chalk.hex('#F24E1E')('\nβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n'));
|
|
50
|
+
|
|
51
|
+
// Save to file
|
|
52
|
+
const filepath = saveReport(topic, result.response);
|
|
53
|
+
console.log(chalk.green(`β Report saved to: ${filepath}`));
|
|
54
|
+
console.log(chalk.gray('\nYou can now use all CLI commands.\n'));
|
|
21
55
|
|
|
22
56
|
} catch (error) {
|
|
23
57
|
spinner.fail(chalk.red('Failed to generate literature review'));
|
|
@@ -31,16 +65,28 @@ async function paper(file) {
|
|
|
31
65
|
return;
|
|
32
66
|
}
|
|
33
67
|
|
|
68
|
+
await showThinking('PAPER_ANALYSIS', file);
|
|
34
69
|
const spinner = ora(`Analyzing paper: ${file}`).start();
|
|
35
70
|
|
|
36
71
|
try {
|
|
37
|
-
|
|
72
|
+
const result = await createMission(`Analyze this paper: ${file}`, 'PAPER_ANALYSIS');
|
|
73
|
+
|
|
38
74
|
spinner.succeed(chalk.green('Paper analysis complete!'));
|
|
39
|
-
|
|
75
|
+
|
|
76
|
+
console.log(chalk.gray(`\nπ Model: ${result.model}`));
|
|
77
|
+
console.log(chalk.gray(`π° RCC Cost: ${result.rccCost}`));
|
|
78
|
+
console.log(chalk.gray(`π³ Remaining Balance: ${result.rccBalance} RCC`));
|
|
79
|
+
|
|
80
|
+
await displayReport(result.response, true);
|
|
81
|
+
|
|
82
|
+
console.log(chalk.hex('#F24E1E')('\nβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n'));
|
|
83
|
+
|
|
84
|
+
const filepath = saveReport(file, result.response);
|
|
85
|
+
console.log(chalk.green(`β Report saved to: ${filepath}\n`));
|
|
40
86
|
|
|
41
87
|
} catch (error) {
|
|
42
88
|
spinner.fail(chalk.red('Failed to analyze paper'));
|
|
43
|
-
console.error(chalk.red(error.message));
|
|
89
|
+
console.error(chalk.red(error.response?.data?.error || error.message));
|
|
44
90
|
}
|
|
45
91
|
}
|
|
46
92
|
|
|
@@ -50,8 +96,29 @@ async function compare(files) {
|
|
|
50
96
|
return;
|
|
51
97
|
}
|
|
52
98
|
|
|
53
|
-
|
|
54
|
-
|
|
99
|
+
await showThinking('COMPARE_PAPERS', files.join(', '));
|
|
100
|
+
const spinner = ora(`Comparing ${files.length} papers`).start();
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const result = await createMission(`Compare these papers: ${files.join(', ')}`, 'COMPARE_PAPERS');
|
|
104
|
+
|
|
105
|
+
spinner.succeed(chalk.green('Paper comparison complete!'));
|
|
106
|
+
|
|
107
|
+
console.log(chalk.gray(`\nπ Model: ${result.model}`));
|
|
108
|
+
console.log(chalk.gray(`π° RCC Cost: ${result.rccCost}`));
|
|
109
|
+
console.log(chalk.gray(`π³ Remaining Balance: ${result.rccBalance} RCC`));
|
|
110
|
+
|
|
111
|
+
await displayReport(result.response, true);
|
|
112
|
+
|
|
113
|
+
console.log(chalk.hex('#F24E1E')('\nβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n'));
|
|
114
|
+
|
|
115
|
+
const filepath = saveReport(`comparison_${files.length}papers`, result.response);
|
|
116
|
+
console.log(chalk.green(`β Report saved to: ${filepath}\n`));
|
|
117
|
+
|
|
118
|
+
} catch (error) {
|
|
119
|
+
spinner.fail(chalk.red('Failed to compare papers'));
|
|
120
|
+
console.error(chalk.red(error.response?.data?.error || error.message));
|
|
121
|
+
}
|
|
55
122
|
}
|
|
56
123
|
|
|
57
124
|
async function gaps(topic) {
|
|
@@ -60,15 +127,24 @@ async function gaps(topic) {
|
|
|
60
127
|
return;
|
|
61
128
|
}
|
|
62
129
|
|
|
130
|
+
await showThinking('RESEARCH_GAP', topic);
|
|
63
131
|
const spinner = ora(`Discovering research gaps for: ${topic}`).start();
|
|
64
132
|
|
|
65
133
|
try {
|
|
66
|
-
const result = await createMission(topic, '
|
|
134
|
+
const result = await createMission(topic, 'RESEARCH_GAP');
|
|
67
135
|
|
|
68
136
|
spinner.succeed(chalk.green('Research gaps discovered!'));
|
|
69
|
-
|
|
70
|
-
console.log(chalk.gray(
|
|
71
|
-
console.log(chalk.
|
|
137
|
+
|
|
138
|
+
console.log(chalk.gray(`\nπ Model: ${result.model}`));
|
|
139
|
+
console.log(chalk.gray(`π° RCC Cost: ${result.rccCost}`));
|
|
140
|
+
console.log(chalk.gray(`π³ Remaining Balance: ${result.rccBalance} RCC`));
|
|
141
|
+
|
|
142
|
+
await displayReport(result.response, true);
|
|
143
|
+
|
|
144
|
+
console.log(chalk.hex('#F24E1E')('\nβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n'));
|
|
145
|
+
|
|
146
|
+
const filepath = saveReport(`gaps_${topic}`, result.response);
|
|
147
|
+
console.log(chalk.green(`β Report saved to: ${filepath}\n`));
|
|
72
148
|
|
|
73
149
|
} catch (error) {
|
|
74
150
|
spinner.fail(chalk.red('Failed to discover research gaps'));
|
|
@@ -82,15 +158,24 @@ async function hypothesis(topic) {
|
|
|
82
158
|
return;
|
|
83
159
|
}
|
|
84
160
|
|
|
161
|
+
await showThinking('HYPOTHESIS', topic);
|
|
85
162
|
const spinner = ora(`Generating hypotheses for: ${topic}`).start();
|
|
86
163
|
|
|
87
164
|
try {
|
|
88
|
-
const result = await createMission(topic, '
|
|
165
|
+
const result = await createMission(topic, 'HYPOTHESIS');
|
|
89
166
|
|
|
90
167
|
spinner.succeed(chalk.green('Hypotheses generated!'));
|
|
91
|
-
|
|
92
|
-
console.log(chalk.gray(
|
|
93
|
-
console.log(chalk.
|
|
168
|
+
|
|
169
|
+
console.log(chalk.gray(`\nπ Model: ${result.model}`));
|
|
170
|
+
console.log(chalk.gray(`π° RCC Cost: ${result.rccCost}`));
|
|
171
|
+
console.log(chalk.gray(`π³ Remaining Balance: ${result.rccBalance} RCC`));
|
|
172
|
+
|
|
173
|
+
await displayReport(result.response, true);
|
|
174
|
+
|
|
175
|
+
console.log(chalk.hex('#F24E1E')('\nβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n'));
|
|
176
|
+
|
|
177
|
+
const filepath = saveReport(`hypothesis_${topic}`, result.response);
|
|
178
|
+
console.log(chalk.green(`β Report saved to: ${filepath}\n`));
|
|
94
179
|
|
|
95
180
|
} catch (error) {
|
|
96
181
|
spinner.fail(chalk.red('Failed to generate hypotheses'));
|
package/src/config.js
CHANGED
|
@@ -6,7 +6,10 @@ const config = new Conf({
|
|
|
6
6
|
apiUrl: 'https://obxvisionassistant-yyedhmslhq-uc.a.run.app',
|
|
7
7
|
dashboardUrl: 'https://x.omnibiofex.cloud/dash',
|
|
8
8
|
authToken: null,
|
|
9
|
+
refreshToken: null, // π₯ NEW: Store refresh token
|
|
9
10
|
userId: null,
|
|
11
|
+
userEmail: null,
|
|
12
|
+
tokenExpiry: null, // π₯ NEW: Track token expiry time
|
|
10
13
|
}
|
|
11
14
|
});
|
|
12
15
|
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple markdown renderer using chalk
|
|
5
|
+
* Handles: **bold**, *italic*, # headings, - lists, `code`, tables
|
|
6
|
+
*/
|
|
7
|
+
function renderMarkdown(markdown) {
|
|
8
|
+
const lines = markdown.split('\n');
|
|
9
|
+
const rendered = [];
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12
|
+
let line = lines[i];
|
|
13
|
+
|
|
14
|
+
// Skip empty lines
|
|
15
|
+
if (line.trim() === '') {
|
|
16
|
+
rendered.push('');
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Headings
|
|
21
|
+
if (line.startsWith('### ')) {
|
|
22
|
+
rendered.push(chalk.hex('#F24E1E').bold('\n' + line.substring(4)));
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (line.startsWith('## ')) {
|
|
26
|
+
rendered.push(chalk.hex('#F24E1E').bold.underline('\n' + line.substring(3)));
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (line.startsWith('# ')) {
|
|
30
|
+
rendered.push(chalk.hex('#F24E1E').bold.underline('\n' + line.substring(2)));
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Horizontal rules
|
|
35
|
+
if (line.trim() === '---' || line.trim() === '***') {
|
|
36
|
+
rendered.push(chalk.gray('β'.repeat(60)));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// List items
|
|
41
|
+
if (line.trim().startsWith('- ') || line.trim().startsWith('* ')) {
|
|
42
|
+
const content = line.trim().substring(2);
|
|
43
|
+
const formatted = formatInlineMarkdown(content);
|
|
44
|
+
rendered.push(` ${chalk.hex('#F24E1E')('βΈ')} ${formatted}`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Numbered lists
|
|
49
|
+
const numberedMatch = line.match(/^(\d+)\.\s+(.+)/);
|
|
50
|
+
if (numberedMatch) {
|
|
51
|
+
const num = numberedMatch[1];
|
|
52
|
+
const content = numberedMatch[2];
|
|
53
|
+
const formatted = formatInlineMarkdown(content);
|
|
54
|
+
rendered.push(` ${chalk.hex('#F24E1E')(num + '.')} ${formatted}`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Table rows (simple rendering)
|
|
59
|
+
if (line.includes('|') && line.trim().startsWith('|')) {
|
|
60
|
+
// Skip separator rows like |---|---|
|
|
61
|
+
if (line.match(/^\|[\s\-:]+\|$/)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const cells = line.split('|').filter(cell => cell.trim() !== '');
|
|
65
|
+
const formattedCells = cells.map(cell => {
|
|
66
|
+
const trimmed = cell.trim();
|
|
67
|
+
return formatInlineMarkdown(trimmed);
|
|
68
|
+
});
|
|
69
|
+
rendered.push(' ' + formattedCells.join(' β '));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Regular text with inline formatting
|
|
74
|
+
rendered.push(formatInlineMarkdown(line));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return rendered.join('\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Format inline markdown: **bold**, *italic*, `code`, [links]
|
|
82
|
+
*/
|
|
83
|
+
function formatInlineMarkdown(text) {
|
|
84
|
+
let result = text;
|
|
85
|
+
|
|
86
|
+
// Bold: **text** or __text__
|
|
87
|
+
result = result.replace(/\*\*(.+?)\*\*/g, chalk.bold.white('$1'));
|
|
88
|
+
result = result.replace(/__(.+?)__/g, chalk.bold.white('$1'));
|
|
89
|
+
|
|
90
|
+
// Italic: *text* or _text_
|
|
91
|
+
result = result.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, chalk.italic('$1'));
|
|
92
|
+
result = result.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, chalk.italic('$1'));
|
|
93
|
+
|
|
94
|
+
// Inline code: `code`
|
|
95
|
+
result = result.replace(/`([^`]+)`/g, chalk.bgBlack.cyan(' $1 '));
|
|
96
|
+
|
|
97
|
+
// Links: [text](url)
|
|
98
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, chalk.blue.underline('$1'));
|
|
99
|
+
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Typing effect - displays text character by character
|
|
105
|
+
*/
|
|
106
|
+
async function typeText(text, speed = 8) {
|
|
107
|
+
for (const char of text) {
|
|
108
|
+
process.stdout.write(char);
|
|
109
|
+
await new Promise(resolve => setTimeout(resolve, speed));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Streaming effect - displays text in chunks (faster than char-by-char)
|
|
115
|
+
*/
|
|
116
|
+
async function streamText(text, chunkSize = 3, delay = 15) {
|
|
117
|
+
for (let i = 0; i < text.length; i += chunkSize) {
|
|
118
|
+
process.stdout.write(text.substring(i, i + chunkSize));
|
|
119
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Shows animated thinking process with multi-agent swarm
|
|
125
|
+
*/
|
|
126
|
+
async function showThinking(taskType, topic) {
|
|
127
|
+
const thinkingSteps = getThinkingSteps(taskType, topic);
|
|
128
|
+
|
|
129
|
+
console.log(chalk.hex('#F24E1E')('\nπ§ OmniBioFex X is thinking...\n'));
|
|
130
|
+
|
|
131
|
+
for (const step of thinkingSteps) {
|
|
132
|
+
const frames = ['β ', 'β ', 'β Ή', 'β Έ', 'β Ό', 'β ΄', 'β ¦', 'β §', 'β ', 'β '];
|
|
133
|
+
let frameIndex = 0;
|
|
134
|
+
|
|
135
|
+
const spinner = setInterval(() => {
|
|
136
|
+
process.stdout.write(`\r ${chalk.hex('#F24E1E')(frames[frameIndex])} ${chalk.gray(step.text)}`);
|
|
137
|
+
frameIndex = (frameIndex + 1) % frames.length;
|
|
138
|
+
}, 80);
|
|
139
|
+
|
|
140
|
+
await new Promise(resolve => setTimeout(resolve, step.duration));
|
|
141
|
+
|
|
142
|
+
clearInterval(spinner);
|
|
143
|
+
process.stdout.write('\r');
|
|
144
|
+
console.log(` ${chalk.green('β')} ${step.text}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(chalk.hex('#F24E1E')('\n⨠Synthesis complete!\n'));
|
|
148
|
+
console.log(chalk.hex('#F24E1E')('βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n'));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get thinking steps based on task type
|
|
153
|
+
*/
|
|
154
|
+
function getThinkingSteps(taskType, topic) {
|
|
155
|
+
const baseSteps = [
|
|
156
|
+
{ text: `π Analyzing research query: "${topic.substring(0, 40)}${topic.length > 40 ? '...' : ''}"`, duration: 600 },
|
|
157
|
+
{ text: 'π Connecting to Deep Reasoning Engineβ’...', duration: 400 },
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
const taskSpecificSteps = {
|
|
161
|
+
LITERATURE_REVIEW: [
|
|
162
|
+
{ text: 'π Literature Agent: Scanning academic databases...', duration: 800 },
|
|
163
|
+
{ text: 'π Reading and extracting key findings from 200+ papers...', duration: 1000 },
|
|
164
|
+
{ text: 'π Mapping citation networks and research lineage...', duration: 700 },
|
|
165
|
+
{ text: 'π Statistician: Validating methodology quality...', duration: 600 },
|
|
166
|
+
{ text: 'π Critic: Identifying contradictions and gaps...', duration: 700 },
|
|
167
|
+
{ text: 'βοΈ Writer: Synthesizing comprehensive review...', duration: 800 },
|
|
168
|
+
],
|
|
169
|
+
RESEARCH_GAP: [
|
|
170
|
+
{ text: 'π Literature Agent: Surveying existing research...', duration: 800 },
|
|
171
|
+
{ text: 'πΊοΈ Domain Expert: Mapping research landscape...', duration: 700 },
|
|
172
|
+
{ text: 'π Identifying underexplored areas...', duration: 900 },
|
|
173
|
+
{ text: 'π Statistician: Analyzing sample size distributions...', duration: 600 },
|
|
174
|
+
{ text: 'π‘ Generating novel research opportunities...', duration: 800 },
|
|
175
|
+
],
|
|
176
|
+
HYPOTHESIS: [
|
|
177
|
+
{ text: 'π Literature Agent: Reviewing theoretical foundations...', duration: 800 },
|
|
178
|
+
{ text: 'π§ Domain Expert: Applying domain knowledge...', duration: 700 },
|
|
179
|
+
{ text: 'π Critic: Stress-testing initial hypotheses...', duration: 800 },
|
|
180
|
+
{ text: 'π Statistician: Designing validation approaches...', duration: 600 },
|
|
181
|
+
{ text: 'π‘ Generating testable hypotheses with rationale...', duration: 700 },
|
|
182
|
+
],
|
|
183
|
+
COMPARE_PAPERS: [
|
|
184
|
+
{ text: 'π Literature Agent: Extracting methodologies...', duration: 700 },
|
|
185
|
+
{ text: 'π Comparing frameworks and approaches...', duration: 800 },
|
|
186
|
+
{ text: 'π Statistician: Analyzing result differences...', duration: 600 },
|
|
187
|
+
{ text: 'π Critic: Evaluating strengths and weaknesses...', duration: 700 },
|
|
188
|
+
{ text: 'βοΈ Writer: Synthesizing comparison matrix...', duration: 600 },
|
|
189
|
+
],
|
|
190
|
+
PAPER_ANALYSIS: [
|
|
191
|
+
{ text: 'π Parsing document structure and content...', duration: 600 },
|
|
192
|
+
{ text: 'π Extracting key claims and evidence...', duration: 700 },
|
|
193
|
+
{ text: 'π Statistician: Evaluating methodology...', duration: 700 },
|
|
194
|
+
{ text: 'π Critic: Identifying limitations...', duration: 600 },
|
|
195
|
+
{ text: 'βοΈ Writer: Generating structured analysis...', duration: 600 },
|
|
196
|
+
],
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const specificSteps = taskSpecificSteps[taskType] || taskSpecificSteps.LITERATURE_REVIEW;
|
|
200
|
+
|
|
201
|
+
return [...baseSteps, ...specificSteps];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Display a complete research report with typing effect
|
|
206
|
+
*/
|
|
207
|
+
async function displayReport(markdown, useTypingEffect = true) {
|
|
208
|
+
const rendered = renderMarkdown(markdown);
|
|
209
|
+
|
|
210
|
+
if (useTypingEffect) {
|
|
211
|
+
const lines = rendered.split('\n');
|
|
212
|
+
for (const line of lines) {
|
|
213
|
+
await streamText(line + '\n', 5, 10);
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
console.log(rendered);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
typeText,
|
|
222
|
+
streamText,
|
|
223
|
+
showThinking,
|
|
224
|
+
renderMarkdown,
|
|
225
|
+
displayReport,
|
|
226
|
+
};
|