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 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.1",
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
- // Add auth header to all requests
11
- apiClient.interceptors.request.use((requestConfig) => {
12
- const token = getAuthToken();
13
- requestConfig.headers.Authorization = `Bearer ${token}`;
14
- return requestConfig;
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, // You can track this later
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 token = await waitForAuth();
94
+ const authData = await waitForAuth();
90
95
 
91
96
  cleanup();
92
97
 
93
- if (token) {
94
- config.set('authToken', token);
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 token received');
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
- const token = url.searchParams.get('token');
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._authToken = token;
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._authToken = null;
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._authToken !== undefined) {
262
+ if (global._authData !== undefined) {
230
263
  clearInterval(checkInterval);
231
- const token = global._authToken;
232
- delete global._authToken;
233
- resolve(token);
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 getAuthToken() {
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 };
@@ -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, 'Academic', 'LITERATURE_REVIEW');
38
+ const result = await createMission(topic, 'LITERATURE_REVIEW');
16
39
 
17
40
  spinner.succeed(chalk.green('Literature review complete!'));
18
- console.log(chalk.gray(`\nRCC Cost: ${result.rccCost}`));
19
- console.log(chalk.gray(`Remaining Balance: ${result.rccBalance}`));
20
- console.log(chalk.hex('#F24E1E')('\nβœ“ Report generated successfully.\n'));
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
- // Would need file upload implementation
72
+ const result = await createMission(`Analyze this paper: ${file}`, 'PAPER_ANALYSIS');
73
+
38
74
  spinner.succeed(chalk.green('Paper analysis complete!'));
39
- console.log(chalk.gray('\nβœ“ Analysis generated successfully.\n'));
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
- console.log(chalk.hex('#F24E1E')(`\nπŸ“Š Comparing ${files.length} papers\n`));
54
- console.log(chalk.gray('Comparison complete!\n'));
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, 'Academic', 'RESEARCH_GAP');
134
+ const result = await createMission(topic, 'RESEARCH_GAP');
67
135
 
68
136
  spinner.succeed(chalk.green('Research gaps discovered!'));
69
- console.log(chalk.gray(`\nRCC Cost: ${result.rccCost}`));
70
- console.log(chalk.gray(`Remaining Balance: ${result.rccBalance}`));
71
- console.log(chalk.hex('#F24E1E')('\nβœ“ Gap analysis complete.\n'));
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, 'Academic', 'HYPOTHESIS');
165
+ const result = await createMission(topic, 'HYPOTHESIS');
89
166
 
90
167
  spinner.succeed(chalk.green('Hypotheses generated!'));
91
- console.log(chalk.gray(`\nRCC Cost: ${result.rccCost}`));
92
- console.log(chalk.gray(`Remaining Balance: ${result.rccBalance}`));
93
- console.log(chalk.hex('#F24E1E')('\nβœ“ Hypothesis generation complete.\n'));
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
+ };