@optiqcode/cli 1.5.0 → 1.6.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 (2) hide show
  1. package/dist/index.js +186 -88
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { isValidDirectory, getGitIgnorePatterns, shouldIgnoreFile } from './util
12
12
  const BACKEND_URL = process.env.OPTIQ_BACKEND_URL || 'https://api.optiqcode.com';
13
13
  async function showBanner() {
14
14
  console.clear();
15
- console.log(chalk.white.bold(`
15
+ console.log(chalk.cyan.bold(`
16
16
  ___ _ _
17
17
  / _ \\ _ __ | |_(_) __ _
18
18
  | | | | '_ \\| __| |/ _\` |
@@ -20,28 +20,49 @@ async function showBanner() {
20
20
  \\___/| .__/ \\__|_|\\__, |
21
21
  |_| |_|
22
22
  `));
23
- console.log(chalk.gray(' Automatic code indexing\n'));
23
+ console.log(chalk.gray(' AI-powered code indexing & search'));
24
+ console.log(chalk.gray(' v1.6.0\n'));
25
+ }
26
+ function showHelp() {
27
+ console.log(chalk.cyan.bold('Optiq CLI') + chalk.gray(' - AI-powered code indexing\n'));
28
+ console.log(chalk.white('Usage:'));
29
+ console.log(chalk.gray(' optiq [command] [options]\n'));
30
+ console.log(chalk.white('Commands:'));
31
+ console.log(chalk.gray(' login Login with email & OTP'));
32
+ console.log(chalk.gray(' index Index current directory once'));
33
+ console.log(chalk.gray(' watch Watch directory and auto-index changes'));
34
+ console.log(chalk.gray(' logout Clear stored credentials'));
35
+ console.log(chalk.gray(' key Show your API key'));
36
+ console.log(chalk.gray(' help Show this help message\n'));
37
+ console.log(chalk.white('Options:'));
38
+ console.log(chalk.gray(' --path <dir> Specify directory to index (default: current)\n'));
39
+ console.log(chalk.white('Examples:'));
40
+ console.log(chalk.gray(' optiq login'));
41
+ console.log(chalk.gray(' optiq index'));
42
+ console.log(chalk.gray(' optiq watch'));
43
+ console.log(chalk.gray(' optiq index --path /path/to/project\n'));
44
+ console.log(chalk.gray('Get started: https://github.com/optiqcode/optiq'));
24
45
  }
25
46
  async function login() {
26
- console.log(chalk.white('šŸ” Passwordless login\n'));
47
+ console.log(chalk.cyan('šŸ” Login\n'));
27
48
  const { email } = await prompts({
28
49
  type: 'text',
29
50
  name: 'email',
30
51
  message: 'Email:',
31
- validate: (value) => value.includes('@') || 'Please enter a valid email',
52
+ validate: (value) => value.includes('@') || 'Invalid email',
32
53
  });
33
54
  if (!email) {
34
- console.log(chalk.gray('\nāš ļø Login cancelled'));
55
+ console.log(chalk.gray('\nCancelled'));
35
56
  process.exit(0);
36
57
  }
37
- const spinner = ora({ text: 'Sending code...', color: 'white' }).start();
58
+ const spinner = ora({ text: 'Sending code...', color: 'cyan' }).start();
38
59
  try {
39
60
  // Send OTP
40
61
  const otpResponse = await axios.post(`${BACKEND_URL}/api/auth/send-otp`, { email }, { timeout: 10000 });
41
62
  if (!otpResponse.data.success) {
42
63
  throw new Error('Failed to send code');
43
64
  }
44
- spinner.succeed(chalk.white('āœ“ Code sent to your email\n'));
65
+ spinner.succeed(chalk.cyan('āœ“ Code sent\n'));
45
66
  const { code } = await prompts({
46
67
  type: 'text',
47
68
  name: 'code',
@@ -52,10 +73,10 @@ async function login() {
52
73
  },
53
74
  });
54
75
  if (!code) {
55
- console.log(chalk.gray('\nāš ļø Login cancelled'));
76
+ console.log(chalk.gray('\nCancelled'));
56
77
  return false;
57
78
  }
58
- const verifySpinner = ora({ text: 'Verifying...', color: 'white' }).start();
79
+ const verifySpinner = ora({ text: 'Verifying...', color: 'cyan' }).start();
59
80
  // Verify OTP
60
81
  const verifyResponse = await axios.post(`${BACKEND_URL}/api/auth/verify-otp`, { email, code }, { timeout: 10000 });
61
82
  if (verifyResponse.data.success) {
@@ -82,29 +103,95 @@ async function login() {
82
103
  token: token,
83
104
  apiKey: validateResponse.data.context_engine_api_key || '',
84
105
  });
85
- verifySpinner.succeed(chalk.white(`āœ“ Logged in as ${email}\n`));
106
+ verifySpinner.succeed(chalk.cyan(`āœ“ Logged in as ${email}\n`));
86
107
  return true;
87
108
  }
88
109
  else {
89
- verifySpinner.fail(chalk.gray('āœ— Invalid code'));
110
+ verifySpinner.fail(chalk.red('āœ— Invalid code'));
90
111
  return false;
91
112
  }
92
113
  }
93
114
  catch (error) {
94
- spinner.fail(chalk.gray('āœ— Login failed'));
115
+ spinner.fail(chalk.red('āœ— Login failed'));
95
116
  if (error.response?.data?.error) {
96
- console.log(chalk.gray(error.response.data.error));
117
+ console.log(chalk.gray(' ' + error.response.data.error));
97
118
  }
98
119
  else {
99
- console.log(chalk.gray(error.message));
120
+ console.log(chalk.gray(' ' + error.message));
100
121
  }
101
122
  return false;
102
123
  }
103
124
  }
104
125
  async function main() {
105
- await showBanner();
106
- // Check if logged in
126
+ const args = process.argv.slice(2);
127
+ const command = args[0];
128
+ // Parse --path option
129
+ const pathIndex = args.indexOf('--path');
130
+ const targetPath = pathIndex >= 0 && args[pathIndex + 1]
131
+ ? path.resolve(args[pathIndex + 1])
132
+ : path.resolve(process.cwd());
133
+ // Handle commands
134
+ if (command === 'help' || command === '--help' || command === '-h') {
135
+ showHelp();
136
+ return;
137
+ }
138
+ if (command === 'login') {
139
+ await showBanner();
140
+ const success = await login();
141
+ process.exit(success ? 0 : 1);
142
+ }
143
+ if (command === 'logout') {
144
+ const { clearConfig } = await import('./utils/config.js');
145
+ await clearConfig();
146
+ console.log(chalk.cyan('āœ“ Logged out successfully'));
147
+ process.exit(0);
148
+ }
149
+ if (command === 'key') {
150
+ const config = await getConfig();
151
+ if (!config) {
152
+ console.log(chalk.red('āœ— Not logged in'));
153
+ console.log(chalk.gray('Run: optiq login'));
154
+ process.exit(1);
155
+ }
156
+ console.log(chalk.cyan('\nšŸ”‘ Your API Key:\n'));
157
+ console.log(chalk.white(config.apiKey || 'No API key found'));
158
+ console.log(chalk.gray('\nUse this for the MCP server or direct API calls'));
159
+ console.log(chalk.gray('Keep this secret!\n'));
160
+ process.exit(0);
161
+ }
162
+ // Check authentication for index/watch commands
107
163
  let config = await getConfig();
164
+ if (command === 'index' || command === 'watch') {
165
+ if (!config) {
166
+ console.log(chalk.red('āœ— Not logged in'));
167
+ console.log(chalk.gray('Run: optiq login'));
168
+ process.exit(1);
169
+ }
170
+ // Safety check
171
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
172
+ if (homeDir && path.resolve(targetPath) === path.resolve(homeDir)) {
173
+ console.log(chalk.red('āœ— Cannot index home directory'));
174
+ console.log(chalk.gray('Specify a project directory'));
175
+ process.exit(1);
176
+ }
177
+ // Validate directory
178
+ const validation = await isValidDirectory(targetPath);
179
+ if (!validation.valid) {
180
+ console.log(chalk.red(`āœ— ${validation.error}`));
181
+ process.exit(1);
182
+ }
183
+ console.log(chalk.cyan('šŸ“ ' + path.basename(targetPath)));
184
+ console.log(chalk.gray(` ${validation.fileCount} files • ${targetPath}\n`));
185
+ if (command === 'index') {
186
+ await indexOnce(targetPath, config);
187
+ }
188
+ else {
189
+ await watchDirectory(targetPath, config);
190
+ }
191
+ return;
192
+ }
193
+ // Interactive mode if no command
194
+ await showBanner();
108
195
  if (!config) {
109
196
  const success = await login();
110
197
  if (!success) {
@@ -116,54 +203,49 @@ async function main() {
116
203
  }
117
204
  }
118
205
  else {
119
- console.log(chalk.white('āœ“ Logged in as'), chalk.white.bold(config.email));
206
+ console.log(chalk.cyan('āœ“ ' + config.email));
120
207
  console.log();
121
208
  }
122
- const targetPath = path.resolve(process.cwd());
123
- // Safety check: prevent indexing home directory
209
+ // Safety check
124
210
  const homeDir = process.env.HOME || process.env.USERPROFILE;
125
211
  if (homeDir && path.resolve(targetPath) === path.resolve(homeDir)) {
126
- console.log(chalk.gray('āœ— Cannot index home directory'));
127
- console.log(chalk.gray('Please run this from a project directory'));
212
+ console.log(chalk.red('āœ— Cannot index home directory'));
128
213
  process.exit(1);
129
214
  }
130
215
  // Validate directory
131
216
  const validation = await isValidDirectory(targetPath);
132
217
  if (!validation.valid) {
133
- console.log(chalk.gray(`āœ— ${validation.error}`));
218
+ console.log(chalk.red(`āœ— ${validation.error}`));
134
219
  process.exit(1);
135
220
  }
136
- console.log(chalk.white('šŸ“ Directory:'), chalk.white.bold(path.basename(targetPath)));
137
- console.log(chalk.gray(` ${validation.fileCount} files found`));
138
- console.log(chalk.gray(` ${targetPath}\n`));
221
+ console.log(chalk.cyan('šŸ“ ' + path.basename(targetPath)));
222
+ console.log(chalk.gray(` ${validation.fileCount} files • ${targetPath}\n`));
139
223
  // Ask what to do
140
224
  const { action } = await prompts({
141
225
  type: 'select',
142
226
  name: 'action',
143
- message: 'What would you like to do?',
227
+ message: 'Choose action:',
144
228
  choices: [
145
- { title: 'šŸ‘€ Watch and auto-index changes', value: 'watch' },
146
229
  { title: 'šŸ“¦ Index once', value: 'index' },
147
- { title: 'šŸ”‘ Show API Key', value: 'show-api-key' },
230
+ { title: 'šŸ‘€ Watch & auto-index', value: 'watch' },
231
+ { title: 'šŸ”‘ Show API Key', value: 'key' },
148
232
  { title: '🚪 Logout', value: 'logout' },
149
- { title: 'āŒ Exit', value: 'exit' },
150
233
  ],
151
234
  });
152
- if (action === 'exit') {
153
- console.log(chalk.gray('\nGoodbye!'));
235
+ if (!action) {
236
+ console.log(chalk.gray('\nCancelled'));
154
237
  process.exit(0);
155
238
  }
156
- if (action === 'show-api-key') {
157
- console.log(chalk.white('\nšŸ”‘ Your Context Engine API Key:\n'));
158
- console.log(chalk.white.bold(config.apiKey || 'No API key found'));
159
- console.log(chalk.gray('\nUse this key to configure the Optiq MCP server or make API calls.'));
160
- console.log(chalk.gray('Keep this key secret and never share it publicly.\n'));
239
+ if (action === 'key') {
240
+ console.log(chalk.cyan('\nšŸ”‘ Your API Key:\n'));
241
+ console.log(chalk.white(config.apiKey));
242
+ console.log(chalk.gray('\nKeep this secret!\n'));
161
243
  process.exit(0);
162
244
  }
163
245
  if (action === 'logout') {
164
246
  const { clearConfig } = await import('./utils/config.js');
165
247
  await clearConfig();
166
- console.log(chalk.white('\nāœ“ Logged out successfully'));
248
+ console.log(chalk.cyan('\nāœ“ Logged out'));
167
249
  process.exit(0);
168
250
  }
169
251
  if (action === 'index') {
@@ -174,53 +256,72 @@ async function main() {
174
256
  }
175
257
  }
176
258
  async function indexOnce(targetPath, config) {
177
- const spinner = ora({ text: 'Collecting files...', color: 'white' }).start();
259
+ const spinner = ora({ text: 'Collecting files...', color: 'cyan' }).start();
178
260
  try {
179
261
  const files = await collectFiles(targetPath);
180
262
  spinner.text = `Reading ${files.length} files...`;
181
263
  const fileContents = {};
264
+ let processed = 0;
182
265
  for (const file of files) {
183
266
  try {
184
267
  const content = await fs.readFile(file, 'utf-8');
185
- const relativePath = path.relative(targetPath, file);
268
+ const relativePath = path.relative(targetPath, file).replace(/\\/g, '/');
186
269
  fileContents[relativePath] = content;
270
+ processed++;
271
+ if (processed % 50 === 0) {
272
+ spinner.text = `Reading files... ${processed}/${files.length}`;
273
+ }
187
274
  }
188
275
  catch (error) {
189
- // Skip files that can't be read
276
+ // Skip unreadable files
190
277
  }
191
278
  }
192
- spinner.text = 'Uploading to Optiq...';
193
279
  const filesArray = Object.entries(fileContents).map(([path, content]) => ({
194
280
  path,
195
281
  content,
196
282
  }));
197
- const response = await axios.post(`${BACKEND_URL}/api/nexus/index/content`, {
198
- repository_path: targetPath,
199
- files: filesArray,
200
- }, {
201
- headers: {
202
- 'X-API-Key': config.apiKey,
203
- 'Content-Type': 'application/json',
204
- },
205
- timeout: 0, // No timeout for large codebases
206
- });
207
- if (response.data.success) {
208
- spinner.succeed(chalk.white('āœ“ Indexing complete'));
209
- console.log(chalk.white('\nšŸ“Š Repository ID:'), chalk.white.bold(response.data.repo_id));
210
- console.log(chalk.gray(' Use this with the MCP server or API\n'));
211
- }
212
- else {
213
- spinner.fail(chalk.gray('āœ— Indexing failed'));
214
- console.log(chalk.gray(response.data.error || 'Unknown error'));
283
+ // Batch upload
284
+ const BATCH_SIZE = 50;
285
+ let totalFiles = 0;
286
+ let totalEntities = 0;
287
+ let repoId = '';
288
+ for (let i = 0; i < filesArray.length; i += BATCH_SIZE) {
289
+ const batch = filesArray.slice(i, i + BATCH_SIZE);
290
+ const batchNum = Math.floor(i / BATCH_SIZE) + 1;
291
+ const totalBatches = Math.ceil(filesArray.length / BATCH_SIZE);
292
+ spinner.text = `Uploading... batch ${batchNum}/${totalBatches}`;
293
+ const response = await axios.post(`${BACKEND_URL}/api/nexus/index/content`, {
294
+ repository_path: targetPath,
295
+ files: batch,
296
+ }, {
297
+ headers: {
298
+ 'X-API-Key': config.apiKey,
299
+ 'Content-Type': 'application/json',
300
+ },
301
+ timeout: 0,
302
+ });
303
+ if (!response.data.success) {
304
+ spinner.fail(chalk.red('āœ— Indexing failed'));
305
+ console.log(chalk.gray(response.data.error || 'Unknown error'));
306
+ return;
307
+ }
308
+ repoId = response.data.repo_id;
309
+ totalFiles += response.data.stats?.files_indexed || 0;
310
+ totalEntities += response.data.stats?.entities_indexed || 0;
215
311
  }
312
+ spinner.succeed(chalk.cyan('āœ“ Indexed'));
313
+ console.log(chalk.gray(` ${totalFiles} files • ${totalEntities} entities`));
314
+ console.log(chalk.cyan('\nšŸ“Š Repository ID:'));
315
+ console.log(chalk.white(` ${repoId}`));
316
+ console.log(chalk.gray('\n Use this ID with the MCP server\n'));
216
317
  }
217
318
  catch (error) {
218
- spinner.fail(chalk.gray('āœ— Indexing failed'));
319
+ spinner.fail(chalk.red('āœ— Failed'));
219
320
  if (error.response?.data?.error) {
220
- console.log(chalk.gray(error.response.data.error));
321
+ console.log(chalk.gray(' ' + error.response.data.error));
221
322
  }
222
323
  else {
223
- console.log(chalk.gray(error.message));
324
+ console.log(chalk.gray(' ' + error.message));
224
325
  }
225
326
  }
226
327
  }
@@ -238,9 +339,8 @@ async function watchDirectory(targetPath, config) {
238
339
  const existingRepo = repoListResponse.data.repositories.find((r) => r.path === targetPath);
239
340
  if (existingRepo) {
240
341
  repoId = existingRepo.id;
241
- console.log(chalk.white('āœ“ Repository already indexed'));
242
- console.log(chalk.white('šŸ“Š Repository ID:'), chalk.white.bold(repoId));
243
- console.log();
342
+ console.log(chalk.cyan('āœ“ Already indexed'));
343
+ console.log(chalk.gray(` Repo ID: ${repoId}\n`));
244
344
  }
245
345
  }
246
346
  }
@@ -300,22 +400,22 @@ async function watchDirectory(targetPath, config) {
300
400
  repoId = response.data.repo_id;
301
401
  uploadedCount += batch.length;
302
402
  }
303
- spinner.succeed(chalk.white(`Indexed ${files.length} files`));
304
- console.log(chalk.gray('Repository ID:'), chalk.white.bold(repoId));
305
- console.log();
403
+ spinner.succeed(chalk.cyan(`āœ“ Indexed ${files.length} files`));
404
+ console.log(chalk.gray(` Repo ID: ${repoId}\n`));
306
405
  }
307
406
  catch (error) {
308
- spinner.fail(chalk.gray('āœ— Initial indexing failed'));
407
+ spinner.fail(chalk.red('āœ— Failed'));
309
408
  if (error.response?.data?.error) {
310
- console.log(chalk.gray(error.response.data.error));
409
+ console.log(chalk.gray(' ' + error.response.data.error));
311
410
  }
312
411
  else {
313
- console.log(chalk.gray(error.message));
412
+ console.log(chalk.gray(' ' + error.message));
314
413
  }
315
414
  return;
316
415
  }
317
416
  }
318
- console.log(chalk.white('šŸ‘€ Watching for changes...\n'));
417
+ console.log(chalk.cyan('šŸ‘€ Watching for changes...'));
418
+ console.log(chalk.gray(' Press Ctrl+C to stop\n'));
319
419
  const ignorePatterns = await getGitIgnorePatterns(targetPath);
320
420
  const watcher = chokidar.watch(targetPath, {
321
421
  ignored: (filePath) => {
@@ -352,17 +452,14 @@ async function watchDirectory(targetPath, config) {
352
452
  const uptime = formatUptime(Date.now() - sessionStartTime);
353
453
  const timeSinceLastIndex = Math.floor((Date.now() - lastIndexedTime) / 1000);
354
454
  const lines = [];
355
- lines.push(chalk.white.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
356
- lines.push(`${chalk.cyan('šŸ“Š Session Stats')}`);
357
- lines.push(` ${chalk.gray('Uptime:')} ${chalk.white(uptime)}`);
358
- lines.push(` ${chalk.gray('Total Indexed:')} ${chalk.white(totalIndexed)} ${chalk.gray('changes')}`);
359
- lines.push(` ${chalk.gray('Unique Files:')} ${chalk.white(allIndexedFiles.size)}`);
360
- lines.push('');
361
- lines.push(`${chalk.green('ā—')} ${chalk.gray('Last Activity:')} ${chalk.white(lastIndexedFile || 'None')}`);
455
+ lines.push(chalk.gray('─'.repeat(50)));
456
+ lines.push(`${chalk.cyan('šŸ‘€ Watching')} ${chalk.gray('•')} ${chalk.white(uptime)} ${chalk.gray('uptime')}`);
457
+ lines.push(`${chalk.gray(' Indexed:')} ${chalk.white(totalIndexed)} ${chalk.gray('changes')} ${chalk.gray('•')} ${chalk.white(allIndexedFiles.size)} ${chalk.gray('files')}`);
362
458
  if (lastIndexedFile) {
363
- lines.push(` ${chalk.gray(`${timeSinceLastIndex}s ago`)}`);
459
+ const timeStr = timeSinceLastIndex < 60 ? `${timeSinceLastIndex}s` : `${Math.floor(timeSinceLastIndex / 60)}m`;
460
+ lines.push(`${chalk.gray(' Last:')} ${chalk.white(lastIndexedFile)} ${chalk.gray(`(${timeStr} ago)`)}`);
364
461
  }
365
- lines.push(chalk.white.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
462
+ lines.push(chalk.gray('─'.repeat(50)));
366
463
  lines.push('');
367
464
  logUpdate(lines.join('\n'));
368
465
  };
@@ -436,14 +533,14 @@ async function watchDirectory(targetPath, config) {
436
533
  }
437
534
  else {
438
535
  logUpdate.clear();
439
- console.log(chalk.gray(`āœ— Indexing failed`));
440
- console.log(chalk.gray(response.data.error));
536
+ console.log(chalk.red(`āœ— Failed to index`));
537
+ console.log(chalk.gray(` ${response.data.error}`));
441
538
  }
442
539
  }
443
540
  catch (error) {
444
541
  logUpdate.clear();
445
- console.log(chalk.gray(`āœ— Indexing failed`));
446
- console.log(chalk.gray(error.response?.data || error.message));
542
+ console.log(chalk.red(`āœ— Failed to index`));
543
+ console.log(chalk.gray(` ${error.response?.data?.error || error.message}`));
447
544
  }
448
545
  isProcessing = false;
449
546
  // Check if there are more pending changes and process them
@@ -471,11 +568,12 @@ async function watchDirectory(targetPath, config) {
471
568
  scheduleProcess();
472
569
  })
473
570
  .on('error', (error) => {
474
- console.log(chalk.gray('āœ— Watcher error:'), error.message);
571
+ logUpdate.clear();
572
+ console.log(chalk.red('āœ— Watcher error:'), chalk.gray(error.message));
475
573
  });
476
- console.log(chalk.gray('Press Ctrl+C to stop\n'));
477
574
  process.on('SIGINT', () => {
478
- console.log(chalk.gray('\n\nāš ļø Stopping watcher...'));
575
+ logUpdate.clear();
576
+ console.log(chalk.cyan('\nāœ“ Stopped watching'));
479
577
  watcher.close();
480
578
  process.exit(0);
481
579
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optiqcode/cli",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "CLI tool for Optiq - automatic code indexing and context engine",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",