@optiqcode/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Optiq
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # Optiq CLI
2
+
3
+ Official CLI tool for [Optiq](https://optiqcode.com) - automatic code indexing and intelligent context engine for your codebase.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @optiqcode/cli
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ 1. **Login to Optiq**
14
+ ```bash
15
+ optiq login
16
+ ```
17
+
18
+ 2. **Index your codebase**
19
+ ```bash
20
+ optiq index
21
+ # Or specify a directory
22
+ optiq index --path ./my-project
23
+ ```
24
+
25
+ 3. **Watch for changes** (optional)
26
+ ```bash
27
+ optiq watch
28
+ # Or specify a directory
29
+ optiq watch --path ./my-project
30
+ ```
31
+
32
+ ## Commands
33
+
34
+ ### `optiq login`
35
+ Authenticate with your Optiq account. You'll need to provide your email and password.
36
+
37
+ ### `optiq logout`
38
+ Sign out from your Optiq account.
39
+
40
+ ### `optiq whoami`
41
+ Display your current logged-in account.
42
+
43
+ ### `optiq index [options]`
44
+ Index your codebase for intelligent search and context retrieval.
45
+
46
+ **Options:**
47
+ - `--path <directory>` - Directory to index (defaults to current directory)
48
+
49
+ **Features:**
50
+ - Incremental indexing (only re-indexes changed files)
51
+ - Respects `.gitignore` patterns
52
+ - Supports multiple languages (TypeScript, JavaScript, Python, Rust, Go, Java, C/C++, and more)
53
+
54
+ ### `optiq watch [options]`
55
+ Watch your codebase for changes and automatically re-index modified files.
56
+
57
+ **Options:**
58
+ - `--path <directory>` - Directory to watch (defaults to current directory)
59
+
60
+ **Features:**
61
+ - Real-time file monitoring
62
+ - Debounced indexing (waits 5 seconds after last change)
63
+ - Automatic incremental updates
64
+
65
+ ## Use Cases
66
+
67
+ - **AI-Powered Code Search**: Use with the Optiq MCP server for intelligent codebase queries
68
+ - **Context Engine**: Provide relevant code context to AI assistants
69
+ - **Documentation**: Keep your code index up-to-date for documentation tools
70
+ - **Code Analysis**: Enable advanced code analysis and insights
71
+
72
+ ## Configuration
73
+
74
+ The CLI stores your authentication token in `~/.optiq/config.json`.
75
+
76
+ ## Requirements
77
+
78
+ - Node.js >= 18.0.0
79
+ - An Optiq account (sign up at [optiqcode.com](https://optiqcode.com))
80
+
81
+ ## Support
82
+
83
+ - Documentation: [docs.optiqcode.com](https://docs.optiqcode.com)
84
+ - Issues: [github.com/optiqcode/optiq/issues](https://github.com/optiqcode/optiq/issues)
85
+ - Email: support@optiqcode.com
86
+
87
+ ## License
88
+
89
+ MIT
@@ -0,0 +1,3 @@
1
+ export declare function login(): Promise<void>;
2
+ export declare function logout(): Promise<void>;
3
+ export declare function whoami(): Promise<void>;
@@ -0,0 +1,73 @@
1
+ import prompts from 'prompts';
2
+ import axios from 'axios';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import { getConfig, saveConfig, clearConfig } from '../utils/config.js';
6
+ const API_URL = 'https://optiqcode.com/api';
7
+ export async function login() {
8
+ // Test comment: verifying incremental indexing works correctly
9
+ console.log(chalk.blue('🔐 Login to Optiq\n'));
10
+ const response = await prompts([
11
+ {
12
+ type: 'text',
13
+ name: 'email',
14
+ message: 'Email:',
15
+ validate: (value) => value.includes('@') || 'Please enter a valid email',
16
+ },
17
+ {
18
+ type: 'password',
19
+ name: 'password',
20
+ message: 'Password:',
21
+ },
22
+ ]);
23
+ if (!response.email || !response.password) {
24
+ console.log(chalk.yellow('\n⚠️ Login cancelled'));
25
+ return;
26
+ }
27
+ const spinner = ora('Logging in...').start();
28
+ try {
29
+ const result = await axios.post(`${API_URL}/auth/login`, {
30
+ email: response.email,
31
+ password: response.password,
32
+ });
33
+ if (result.data.success) {
34
+ await saveConfig({
35
+ email: response.email,
36
+ token: result.data.token,
37
+ apiKey: result.data.context_engine_api_key,
38
+ });
39
+ spinner.succeed(chalk.green(`✓ Logged in as ${response.email}`));
40
+ }
41
+ else {
42
+ spinner.fail(chalk.red('✗ Login failed'));
43
+ console.log(chalk.red(result.data.error || 'Unknown error'));
44
+ }
45
+ }
46
+ catch (error) {
47
+ spinner.fail(chalk.red('✗ Login failed'));
48
+ if (error.response?.data?.error) {
49
+ console.log(chalk.red(error.response.data.error));
50
+ }
51
+ else {
52
+ console.log(chalk.red(error.message));
53
+ }
54
+ }
55
+ }
56
+ export async function logout() {
57
+ const config = await getConfig();
58
+ if (!config) {
59
+ console.log(chalk.yellow('⚠️ Not logged in'));
60
+ return;
61
+ }
62
+ await clearConfig();
63
+ console.log(chalk.green('✓ Logged out successfully'));
64
+ }
65
+ export async function whoami() {
66
+ const config = await getConfig();
67
+ if (!config) {
68
+ console.log(chalk.yellow('⚠️ Not logged in'));
69
+ console.log(chalk.dim('Run `optiq login` to get started'));
70
+ return;
71
+ }
72
+ console.log(chalk.blue('📧 Logged in as:'), chalk.bold(config.email));
73
+ }
@@ -0,0 +1,5 @@
1
+ interface IndexOptions {
2
+ path?: string;
3
+ }
4
+ export declare function index(options: IndexOptions): Promise<void>;
5
+ export {};
@@ -0,0 +1,121 @@
1
+ import prompts from 'prompts';
2
+ import axios from 'axios';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import path from 'path';
6
+ import fs from 'fs/promises';
7
+ import { getConfig } from '../utils/config.js';
8
+ import { isValidDirectory, getGitIgnorePatterns, shouldIgnoreFile } from '../utils/files.js';
9
+ const API_URL = 'https://optiqcode.com/api';
10
+ export async function index(options) {
11
+ const config = await getConfig();
12
+ if (!config) {
13
+ console.log(chalk.red('✗ Not logged in'));
14
+ console.log(chalk.dim('Run `optiq login` first'));
15
+ return;
16
+ }
17
+ const targetPath = path.resolve(options.path || process.cwd());
18
+ // Safety check: prevent indexing home directory
19
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
20
+ if (homeDir && path.resolve(targetPath) === path.resolve(homeDir)) {
21
+ console.log(chalk.red('✗ Cannot index home directory'));
22
+ console.log(chalk.dim('Please specify a project directory'));
23
+ return;
24
+ }
25
+ // Validate directory
26
+ const validation = await isValidDirectory(targetPath);
27
+ if (!validation.valid) {
28
+ console.log(chalk.red(`✗ ${validation.error}`));
29
+ return;
30
+ }
31
+ console.log(chalk.blue('📁 Directory:'), chalk.bold(targetPath));
32
+ console.log(chalk.dim(` ${validation.fileCount} files found\n`));
33
+ // Ask for confirmation
34
+ const { confirm } = await prompts({
35
+ type: 'confirm',
36
+ name: 'confirm',
37
+ message: 'Index this directory?',
38
+ initial: true,
39
+ });
40
+ if (!confirm) {
41
+ console.log(chalk.yellow('\n⚠️ Indexing cancelled'));
42
+ return;
43
+ }
44
+ const spinner = ora('Collecting files...').start();
45
+ try {
46
+ // Collect all files
47
+ const files = await collectFiles(targetPath);
48
+ spinner.text = `Reading ${files.length} files...`;
49
+ // Read file contents
50
+ const fileContents = [];
51
+ for (const file of files) {
52
+ try {
53
+ const content = await fs.readFile(file, 'utf-8');
54
+ const relativePath = path.relative(targetPath, file);
55
+ // CRITICAL FIX: Normalize path separators to forward slashes for cross-platform consistency
56
+ const normalizedPath = relativePath.replace(/\\/g, '/');
57
+ fileContents.push({
58
+ path: normalizedPath,
59
+ content: content,
60
+ });
61
+ }
62
+ catch (error) {
63
+ // Skip files that can't be read
64
+ }
65
+ }
66
+ spinner.text = 'Uploading to Optiq...';
67
+ // Send to API
68
+ const response = await axios.post(`${API_URL}/nexus/index/content`, {
69
+ repository_path: targetPath,
70
+ files: fileContents,
71
+ }, {
72
+ headers: {
73
+ Authorization: `Bearer ${config.apiKey}`,
74
+ 'Content-Type': 'application/json',
75
+ },
76
+ timeout: 120000,
77
+ });
78
+ if (response.data.success) {
79
+ spinner.succeed(chalk.green('✓ Indexing complete'));
80
+ console.log(chalk.blue('📊 Repository ID:'), chalk.bold(response.data.repo_id));
81
+ console.log(chalk.blue('📁 Files indexed:'), chalk.bold(response.data.files_processed));
82
+ console.log(chalk.blue('📝 Entities indexed:'), chalk.bold(response.data.entities_indexed));
83
+ console.log(chalk.dim('\nUse this repo_id with the MCP server or API'));
84
+ }
85
+ else {
86
+ spinner.fail(chalk.red('✗ Indexing failed'));
87
+ console.log(chalk.red(response.data.error || 'Unknown error'));
88
+ }
89
+ }
90
+ catch (error) {
91
+ spinner.fail(chalk.red('✗ Indexing failed'));
92
+ if (error.response?.data?.error) {
93
+ console.log(chalk.red(error.response.data.error));
94
+ }
95
+ else {
96
+ console.log(chalk.red(error.message));
97
+ }
98
+ }
99
+ }
100
+ async function collectFiles(dir) {
101
+ const files = [];
102
+ const ignorePatterns = await getGitIgnorePatterns(dir);
103
+ async function walk(currentPath) {
104
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
105
+ for (const entry of entries) {
106
+ const fullPath = path.join(currentPath, entry.name);
107
+ const relativePath = path.relative(dir, fullPath);
108
+ if (shouldIgnoreFile(relativePath, ignorePatterns)) {
109
+ continue;
110
+ }
111
+ if (entry.isDirectory()) {
112
+ await walk(fullPath);
113
+ }
114
+ else if (entry.isFile()) {
115
+ files.push(fullPath);
116
+ }
117
+ }
118
+ }
119
+ await walk(dir);
120
+ return files;
121
+ }
@@ -0,0 +1,5 @@
1
+ interface WatchOptions {
2
+ path?: string;
3
+ }
4
+ export declare function watch(options: WatchOptions): Promise<void>;
5
+ export {};
@@ -0,0 +1,156 @@
1
+ import prompts from 'prompts';
2
+ import axios from 'axios';
3
+ import chalk from 'chalk';
4
+ import chokidar from 'chokidar';
5
+ import path from 'path';
6
+ import fs from 'fs/promises';
7
+ import { getConfig } from '../utils/config.js';
8
+ import { isValidDirectory, getGitIgnorePatterns, shouldIgnoreFile } from '../utils/files.js';
9
+ const API_URL = 'https://optiqcode.com/api';
10
+ export async function watch(options) {
11
+ const config = await getConfig();
12
+ if (!config) {
13
+ console.log(chalk.red('✗ Not logged in'));
14
+ console.log(chalk.dim('Run `optiq login` first'));
15
+ return;
16
+ }
17
+ const targetPath = path.resolve(options.path || process.cwd());
18
+ // Safety check: prevent watching home directory
19
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
20
+ if (homeDir && path.resolve(targetPath) === path.resolve(homeDir)) {
21
+ console.log(chalk.red('✗ Cannot watch home directory'));
22
+ console.log(chalk.dim('Please specify a project directory'));
23
+ return;
24
+ }
25
+ // Validate directory
26
+ const validation = await isValidDirectory(targetPath);
27
+ if (!validation.valid) {
28
+ console.log(chalk.red(`✗ ${validation.error}`));
29
+ return;
30
+ }
31
+ console.log(chalk.blue('📁 Directory:'), chalk.bold(targetPath));
32
+ console.log(chalk.dim(` ${validation.fileCount} files found\n`));
33
+ // Ask for confirmation
34
+ const { confirm } = await prompts({
35
+ type: 'confirm',
36
+ name: 'confirm',
37
+ message: 'Start watching this directory?',
38
+ initial: true,
39
+ });
40
+ if (!confirm) {
41
+ console.log(chalk.yellow('\n⚠️ Watch cancelled'));
42
+ return;
43
+ }
44
+ console.log(chalk.green('✓ Starting file watcher...\n'));
45
+ const ignorePatterns = await getGitIgnorePatterns(targetPath);
46
+ // Create watcher
47
+ const watcher = chokidar.watch(targetPath, {
48
+ ignored: (filePath) => {
49
+ const relativePath = path.relative(targetPath, filePath);
50
+ return shouldIgnoreFile(relativePath, ignorePatterns);
51
+ },
52
+ persistent: true,
53
+ ignoreInitial: true,
54
+ awaitWriteFinish: {
55
+ stabilityThreshold: 2000,
56
+ pollInterval: 100,
57
+ },
58
+ });
59
+ let repoId = null;
60
+ const pendingChanges = new Map();
61
+ let debounceTimer = null;
62
+ const processChanges = async () => {
63
+ if (pendingChanges.size === 0)
64
+ return;
65
+ const changes = Array.from(pendingChanges.entries());
66
+ pendingChanges.clear();
67
+ console.log(chalk.blue(`\n📝 Processing ${changes.length} changes...`));
68
+ try {
69
+ const fileContents = [];
70
+ for (const [filePath, changeType] of changes) {
71
+ const relativePath = path.relative(targetPath, filePath);
72
+ // CRITICAL FIX: Normalize path separators to forward slashes for cross-platform consistency
73
+ const normalizedPath = relativePath.replace(/\\/g, '/');
74
+ if (changeType === 'unlink') {
75
+ fileContents.push({
76
+ path: normalizedPath,
77
+ content: null,
78
+ });
79
+ }
80
+ else {
81
+ try {
82
+ const content = await fs.readFile(filePath, 'utf-8');
83
+ fileContents.push({
84
+ path: normalizedPath,
85
+ content: content,
86
+ });
87
+ }
88
+ catch (error) {
89
+ // Skip files that can't be read
90
+ }
91
+ }
92
+ }
93
+ const response = await axios.post(`${API_URL}/nexus/index/incremental`, {
94
+ repository_id: repoId,
95
+ repository_path: targetPath,
96
+ files: fileContents,
97
+ }, {
98
+ headers: {
99
+ Authorization: `Bearer ${config.apiKey}`,
100
+ 'Content-Type': 'application/json',
101
+ },
102
+ timeout: 60000,
103
+ });
104
+ if (response.data.success) {
105
+ if (!repoId) {
106
+ repoId = response.data.repo_id || response.data.repository_id;
107
+ console.log(chalk.blue('📊 Repository ID:'), chalk.bold(repoId));
108
+ }
109
+ console.log(chalk.green(`✓ Indexed ${response.data.files_processed || changes.length} files, ${response.data.entities_indexed || 0} entities`));
110
+ }
111
+ else {
112
+ console.log(chalk.red('✗ Indexing failed:'), response.data.error);
113
+ }
114
+ }
115
+ catch (error) {
116
+ console.log(chalk.red('✗ Indexing failed:'), error.message);
117
+ }
118
+ };
119
+ const scheduleProcess = () => {
120
+ if (debounceTimer) {
121
+ clearTimeout(debounceTimer);
122
+ }
123
+ // Wait 5 seconds after last change before indexing to avoid spam
124
+ debounceTimer = setTimeout(processChanges, 5000);
125
+ };
126
+ watcher
127
+ .on('add', (filePath) => {
128
+ const relativePath = path.relative(targetPath, filePath);
129
+ console.log(chalk.dim(` + ${relativePath}`));
130
+ pendingChanges.set(filePath, 'add');
131
+ scheduleProcess();
132
+ })
133
+ .on('change', (filePath) => {
134
+ const relativePath = path.relative(targetPath, filePath);
135
+ console.log(chalk.dim(` ~ ${relativePath}`));
136
+ pendingChanges.set(filePath, 'change');
137
+ scheduleProcess();
138
+ })
139
+ .on('unlink', (filePath) => {
140
+ const relativePath = path.relative(targetPath, filePath);
141
+ console.log(chalk.dim(` - ${relativePath}`));
142
+ pendingChanges.set(filePath, 'unlink');
143
+ scheduleProcess();
144
+ })
145
+ .on('error', (error) => {
146
+ console.log(chalk.red('✗ Watcher error:'), error.message);
147
+ });
148
+ console.log(chalk.green('👀 Watching for changes...'));
149
+ console.log(chalk.dim('Press Ctrl+C to stop\n'));
150
+ // Keep process alive
151
+ process.on('SIGINT', () => {
152
+ console.log(chalk.yellow('\n\n⚠️ Stopping watcher...'));
153
+ watcher.close();
154
+ process.exit(0);
155
+ });
156
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,498 @@
1
+ #!/usr/bin/env node
2
+ import prompts from 'prompts';
3
+ import axios from 'axios';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import chokidar from 'chokidar';
7
+ import path from 'path';
8
+ import fs from 'fs/promises';
9
+ import logUpdate from 'log-update';
10
+ import { getConfig, saveConfig } from './utils/config.js';
11
+ import { isValidDirectory, getGitIgnorePatterns, shouldIgnoreFile } from './utils/files.js';
12
+ const BACKEND_URL = process.env.OPTIQ_BACKEND_URL || 'http://localhost:3002';
13
+ async function showBanner() {
14
+ console.clear();
15
+ console.log(chalk.white.bold(`
16
+ ___ _ _
17
+ / _ \\ _ __ | |_(_) __ _
18
+ | | | | '_ \\| __| |/ _\` |
19
+ | |_| | |_) | |_| | (_| |
20
+ \\___/| .__/ \\__|_|\\__, |
21
+ |_| |_|
22
+ `));
23
+ console.log(chalk.gray(' Automatic code indexing\n'));
24
+ }
25
+ async function login() {
26
+ console.log(chalk.white('🔐 Passwordless login\n'));
27
+ const { email } = await prompts({
28
+ type: 'text',
29
+ name: 'email',
30
+ message: 'Email:',
31
+ validate: (value) => value.includes('@') || 'Please enter a valid email',
32
+ });
33
+ if (!email) {
34
+ console.log(chalk.gray('\n⚠️ Login cancelled'));
35
+ process.exit(0);
36
+ }
37
+ const spinner = ora({ text: 'Sending code...', color: 'white' }).start();
38
+ try {
39
+ // Send OTP
40
+ const otpResponse = await axios.post(`${BACKEND_URL}/api/auth/send-otp`, { email }, { timeout: 10000 });
41
+ if (!otpResponse.data.success) {
42
+ throw new Error('Failed to send code');
43
+ }
44
+ spinner.succeed(chalk.white('✓ Code sent to your email\n'));
45
+ const { code } = await prompts({
46
+ type: 'text',
47
+ name: 'code',
48
+ message: 'Enter code:',
49
+ validate: (value) => {
50
+ const cleaned = value.replace(/\s/g, '');
51
+ return cleaned.length >= 5 || 'Invalid code';
52
+ },
53
+ });
54
+ if (!code) {
55
+ console.log(chalk.gray('\n⚠️ Login cancelled'));
56
+ return false;
57
+ }
58
+ const verifySpinner = ora({ text: 'Verifying...', color: 'white' }).start();
59
+ // Verify OTP
60
+ const verifyResponse = await axios.post(`${BACKEND_URL}/api/auth/verify-otp`, { email, code }, { timeout: 10000 });
61
+ if (verifyResponse.data.success) {
62
+ // Get token from response body or cookie
63
+ let token = verifyResponse.data.auth_token;
64
+ // If no token in body, extract from cookie
65
+ if (!token || token === '') {
66
+ const cookies = verifyResponse.headers['set-cookie'];
67
+ if (cookies) {
68
+ const authCookie = cookies.find((c) => c.startsWith('auth_token='));
69
+ if (authCookie) {
70
+ token = authCookie.split(';')[0].split('=')[1];
71
+ }
72
+ }
73
+ }
74
+ if (!token) {
75
+ verifySpinner.fail(chalk.gray('✗ No token received'));
76
+ return false;
77
+ }
78
+ // Get API key by validating the token
79
+ const validateResponse = await axios.post(`${BACKEND_URL}/api/auth/validate`, { token }, { timeout: 5000 });
80
+ await saveConfig({
81
+ email: email,
82
+ token: token,
83
+ apiKey: validateResponse.data.context_engine_api_key || '',
84
+ });
85
+ verifySpinner.succeed(chalk.white(`✓ Logged in as ${email}\n`));
86
+ return true;
87
+ }
88
+ else {
89
+ verifySpinner.fail(chalk.gray('✗ Invalid code'));
90
+ return false;
91
+ }
92
+ }
93
+ catch (error) {
94
+ spinner.fail(chalk.gray('✗ Login failed'));
95
+ if (error.response?.data?.error) {
96
+ console.log(chalk.gray(error.response.data.error));
97
+ }
98
+ else {
99
+ console.log(chalk.gray(error.message));
100
+ }
101
+ return false;
102
+ }
103
+ }
104
+ async function main() {
105
+ await showBanner();
106
+ // Check if logged in
107
+ let config = await getConfig();
108
+ if (!config) {
109
+ const success = await login();
110
+ if (!success) {
111
+ process.exit(1);
112
+ }
113
+ config = await getConfig();
114
+ if (!config) {
115
+ process.exit(1);
116
+ }
117
+ }
118
+ else {
119
+ console.log(chalk.white('✓ Logged in as'), chalk.white.bold(config.email));
120
+ console.log();
121
+ }
122
+ const targetPath = path.resolve(process.cwd());
123
+ // Safety check: prevent indexing home directory
124
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
125
+ 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'));
128
+ process.exit(1);
129
+ }
130
+ // Validate directory
131
+ const validation = await isValidDirectory(targetPath);
132
+ if (!validation.valid) {
133
+ console.log(chalk.gray(`✗ ${validation.error}`));
134
+ process.exit(1);
135
+ }
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`));
139
+ // Ask what to do
140
+ const { action } = await prompts({
141
+ type: 'select',
142
+ name: 'action',
143
+ message: 'What would you like to do?',
144
+ choices: [
145
+ { title: '👀 Watch and auto-index changes', value: 'watch' },
146
+ { title: '📦 Index once', value: 'index' },
147
+ { title: '🚪 Logout', value: 'logout' },
148
+ { title: '❌ Exit', value: 'exit' },
149
+ ],
150
+ });
151
+ if (action === 'exit') {
152
+ console.log(chalk.gray('\nGoodbye!'));
153
+ process.exit(0);
154
+ }
155
+ if (action === 'logout') {
156
+ const { clearConfig } = await import('./utils/config.js');
157
+ await clearConfig();
158
+ console.log(chalk.white('\n✓ Logged out successfully'));
159
+ process.exit(0);
160
+ }
161
+ if (action === 'index') {
162
+ await indexOnce(targetPath, config);
163
+ }
164
+ else if (action === 'watch') {
165
+ await watchDirectory(targetPath, config);
166
+ }
167
+ }
168
+ async function indexOnce(targetPath, config) {
169
+ const spinner = ora({ text: 'Collecting files...', color: 'white' }).start();
170
+ try {
171
+ const files = await collectFiles(targetPath);
172
+ spinner.text = `Reading ${files.length} files...`;
173
+ const fileContents = {};
174
+ for (const file of files) {
175
+ try {
176
+ const content = await fs.readFile(file, 'utf-8');
177
+ const relativePath = path.relative(targetPath, file);
178
+ fileContents[relativePath] = content;
179
+ }
180
+ catch (error) {
181
+ // Skip files that can't be read
182
+ }
183
+ }
184
+ spinner.text = 'Uploading to Optiq...';
185
+ const filesArray = Object.entries(fileContents).map(([path, content]) => ({
186
+ path,
187
+ content,
188
+ }));
189
+ const response = await axios.post(`${BACKEND_URL}/api/nexus/index/content`, {
190
+ repository_path: targetPath,
191
+ files: filesArray,
192
+ }, {
193
+ headers: {
194
+ 'X-API-Key': config.apiKey,
195
+ 'Content-Type': 'application/json',
196
+ },
197
+ timeout: 120000,
198
+ });
199
+ if (response.data.success) {
200
+ spinner.succeed(chalk.white('✓ Indexing complete'));
201
+ console.log(chalk.white('\n📊 Repository ID:'), chalk.white.bold(response.data.repo_id));
202
+ console.log(chalk.gray(' Use this with the MCP server or API\n'));
203
+ }
204
+ else {
205
+ spinner.fail(chalk.gray('✗ Indexing failed'));
206
+ console.log(chalk.gray(response.data.error || 'Unknown error'));
207
+ }
208
+ }
209
+ catch (error) {
210
+ spinner.fail(chalk.gray('✗ Indexing failed'));
211
+ if (error.response?.data?.error) {
212
+ console.log(chalk.gray(error.response.data.error));
213
+ }
214
+ else {
215
+ console.log(chalk.gray(error.message));
216
+ }
217
+ }
218
+ }
219
+ async function watchDirectory(targetPath, config) {
220
+ // Check if repo is already indexed
221
+ let repoId = null;
222
+ try {
223
+ const repoListResponse = await axios.get(`${BACKEND_URL}/api/repositories`, {
224
+ headers: {
225
+ 'X-API-Key': config.apiKey,
226
+ },
227
+ timeout: 10000,
228
+ });
229
+ if (repoListResponse.data.success && repoListResponse.data.repositories) {
230
+ const existingRepo = repoListResponse.data.repositories.find((r) => r.path === targetPath);
231
+ if (existingRepo) {
232
+ repoId = existingRepo.id;
233
+ console.log(chalk.white('✓ Repository already indexed'));
234
+ console.log(chalk.white('📊 Repository ID:'), chalk.white.bold(repoId));
235
+ console.log();
236
+ }
237
+ }
238
+ }
239
+ catch (error) {
240
+ // Ignore errors, will do full index
241
+ }
242
+ // Only do full index if repo doesn't exist
243
+ if (!repoId) {
244
+ const spinner = ora({ text: 'Collecting files...', color: 'white' }).start();
245
+ try {
246
+ const files = await collectFiles(targetPath);
247
+ spinner.text = `Indexing ${files.length} files...`;
248
+ const fileContents = {};
249
+ let processed = 0;
250
+ for (const file of files) {
251
+ try {
252
+ const content = await fs.readFile(file, 'utf-8');
253
+ const relativePath = path.relative(targetPath, file);
254
+ fileContents[relativePath] = content;
255
+ processed++;
256
+ // Update progress
257
+ const percent = Math.round((processed / files.length) * 100);
258
+ spinner.text = `Indexing ${processed}/${files.length} files (${percent}%)`;
259
+ }
260
+ catch (error) {
261
+ // Skip files that can't be read
262
+ }
263
+ }
264
+ spinner.text = 'Uploading to Optiq...';
265
+ const filesArray = Object.entries(fileContents).map(([path, content]) => ({
266
+ path,
267
+ content,
268
+ }));
269
+ const response = await axios.post(`${BACKEND_URL}/api/nexus/index/content`, {
270
+ repository_path: targetPath,
271
+ files: filesArray,
272
+ }, {
273
+ headers: {
274
+ 'X-API-Key': config.apiKey,
275
+ 'Content-Type': 'application/json',
276
+ },
277
+ timeout: 120000,
278
+ });
279
+ if (response.data.success) {
280
+ repoId = response.data.repo_id;
281
+ spinner.succeed(chalk.white(`Indexed ${files.length} files`));
282
+ console.log(chalk.gray('Repository ID:'), chalk.white.bold(repoId));
283
+ console.log();
284
+ }
285
+ else {
286
+ spinner.fail(chalk.gray('Initial indexing failed'));
287
+ console.log(chalk.gray(response.data.error || 'Unknown error'));
288
+ return;
289
+ }
290
+ }
291
+ catch (error) {
292
+ spinner.fail(chalk.gray('✗ Initial indexing failed'));
293
+ if (error.response?.data?.error) {
294
+ console.log(chalk.gray(error.response.data.error));
295
+ }
296
+ else {
297
+ console.log(chalk.gray(error.message));
298
+ }
299
+ return;
300
+ }
301
+ }
302
+ console.log(chalk.white('👀 Watching for changes...\n'));
303
+ const ignorePatterns = await getGitIgnorePatterns(targetPath);
304
+ const watcher = chokidar.watch(targetPath, {
305
+ ignored: (filePath) => {
306
+ const relativePath = path.relative(targetPath, filePath);
307
+ return shouldIgnoreFile(relativePath, ignorePatterns);
308
+ },
309
+ persistent: true,
310
+ ignoreInitial: true,
311
+ awaitWriteFinish: {
312
+ stabilityThreshold: 300,
313
+ pollInterval: 50,
314
+ },
315
+ });
316
+ const pendingChanges = new Map();
317
+ let debounceTimer = null;
318
+ const allIndexedFiles = new Set();
319
+ const fileContentCache = new Map();
320
+ let isProcessing = false;
321
+ let totalIndexed = 0;
322
+ let sessionStartTime = Date.now();
323
+ let lastIndexedFile = '';
324
+ let lastIndexedTime = Date.now();
325
+ const formatUptime = (ms) => {
326
+ const seconds = Math.floor(ms / 1000);
327
+ const minutes = Math.floor(seconds / 60);
328
+ const hours = Math.floor(minutes / 60);
329
+ if (hours > 0)
330
+ return `${hours}h ${minutes % 60}m`;
331
+ if (minutes > 0)
332
+ return `${minutes}m ${seconds % 60}s`;
333
+ return `${seconds}s`;
334
+ };
335
+ const updateDashboard = () => {
336
+ const uptime = formatUptime(Date.now() - sessionStartTime);
337
+ const timeSinceLastIndex = Math.floor((Date.now() - lastIndexedTime) / 1000);
338
+ const lines = [];
339
+ lines.push(chalk.white.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
340
+ lines.push(`${chalk.cyan('📊 Session Stats')}`);
341
+ lines.push(` ${chalk.gray('Uptime:')} ${chalk.white(uptime)}`);
342
+ lines.push(` ${chalk.gray('Total Indexed:')} ${chalk.white(totalIndexed)} ${chalk.gray('changes')}`);
343
+ lines.push(` ${chalk.gray('Unique Files:')} ${chalk.white(allIndexedFiles.size)}`);
344
+ lines.push('');
345
+ lines.push(`${chalk.green('●')} ${chalk.gray('Last Activity:')} ${chalk.white(lastIndexedFile || 'None')}`);
346
+ if (lastIndexedFile) {
347
+ lines.push(` ${chalk.gray(`${timeSinceLastIndex}s ago`)}`);
348
+ }
349
+ lines.push(chalk.white.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
350
+ lines.push('');
351
+ logUpdate(lines.join('\n'));
352
+ };
353
+ // Update dashboard every 5 seconds to show uptime
354
+ const dashboardInterval = setInterval(updateDashboard, 5000);
355
+ const processChanges = async () => {
356
+ if (pendingChanges.size === 0 || isProcessing)
357
+ return;
358
+ isProcessing = true;
359
+ const changes = Array.from(pendingChanges.entries());
360
+ pendingChanges.clear();
361
+ // Collect unique files first
362
+ const uniqueFiles = new Set();
363
+ for (const [filePath] of changes) {
364
+ const relativePath = path.relative(targetPath, filePath);
365
+ uniqueFiles.add(relativePath);
366
+ allIndexedFiles.add(relativePath);
367
+ }
368
+ const fileList = Array.from(uniqueFiles);
369
+ try {
370
+ const filesArray = [];
371
+ let hasChanges = false;
372
+ for (const [filePath, changeType] of changes) {
373
+ const relativePath = path.relative(targetPath, filePath);
374
+ if (changeType === 'unlink') {
375
+ filesArray.push({ path: relativePath, content: null });
376
+ fileContentCache.delete(relativePath);
377
+ hasChanges = true;
378
+ }
379
+ else {
380
+ try {
381
+ const content = await fs.readFile(filePath, 'utf-8');
382
+ // Skip if content hasn't changed
383
+ if (fileContentCache.get(relativePath) === content) {
384
+ continue;
385
+ }
386
+ fileContentCache.set(relativePath, content);
387
+ filesArray.push({ path: relativePath, content });
388
+ hasChanges = true;
389
+ }
390
+ catch (error) {
391
+ // Skip files that can't be read
392
+ }
393
+ }
394
+ }
395
+ // If no actual changes, skip indexing
396
+ if (!hasChanges || filesArray.length === 0) {
397
+ isProcessing = false;
398
+ return;
399
+ }
400
+ const response = await axios.post(`${BACKEND_URL}/api/nexus/index/incremental`, {
401
+ repository_id: repoId,
402
+ repository_path: targetPath,
403
+ files: filesArray,
404
+ }, {
405
+ headers: {
406
+ 'X-API-Key': config.apiKey,
407
+ 'Content-Type': 'application/json',
408
+ },
409
+ timeout: 60000,
410
+ });
411
+ if (response.data.success) {
412
+ totalIndexed += filesArray.length;
413
+ lastIndexedTime = Date.now();
414
+ // Update last indexed file
415
+ if (filesArray.length === 1) {
416
+ lastIndexedFile = filesArray[0].path;
417
+ }
418
+ else {
419
+ lastIndexedFile = `${filesArray.length} files`;
420
+ }
421
+ // Update dashboard
422
+ updateDashboard();
423
+ }
424
+ else {
425
+ logUpdate.clear();
426
+ console.log(chalk.gray(`✗ Indexing failed`));
427
+ console.log(chalk.gray(response.data.error));
428
+ }
429
+ }
430
+ catch (error) {
431
+ logUpdate.clear();
432
+ console.log(chalk.gray(`✗ Indexing failed`));
433
+ console.log(chalk.gray(error.response?.data || error.message));
434
+ }
435
+ isProcessing = false;
436
+ // Check if there are more pending changes and process them
437
+ if (pendingChanges.size > 0) {
438
+ scheduleProcess();
439
+ }
440
+ };
441
+ const scheduleProcess = () => {
442
+ if (debounceTimer) {
443
+ clearTimeout(debounceTimer);
444
+ }
445
+ debounceTimer = setTimeout(processChanges, 50); // Batch rapid changes
446
+ };
447
+ watcher
448
+ .on('add', (filePath) => {
449
+ const relativePath = path.relative(targetPath, filePath);
450
+ pendingChanges.set(filePath, 'add');
451
+ scheduleProcess();
452
+ })
453
+ .on('change', (filePath) => {
454
+ const relativePath = path.relative(targetPath, filePath);
455
+ pendingChanges.set(filePath, 'change');
456
+ scheduleProcess();
457
+ })
458
+ .on('unlink', (filePath) => {
459
+ const relativePath = path.relative(targetPath, filePath);
460
+ pendingChanges.set(filePath, 'unlink');
461
+ scheduleProcess();
462
+ })
463
+ .on('error', (error) => {
464
+ console.log(chalk.gray('✗ Watcher error:'), error.message);
465
+ });
466
+ console.log(chalk.gray('Press Ctrl+C to stop\n'));
467
+ process.on('SIGINT', () => {
468
+ console.log(chalk.gray('\n\n⚠️ Stopping watcher...'));
469
+ watcher.close();
470
+ process.exit(0);
471
+ });
472
+ }
473
+ async function collectFiles(dir) {
474
+ const files = [];
475
+ const ignorePatterns = await getGitIgnorePatterns(dir);
476
+ async function walk(currentPath) {
477
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
478
+ for (const entry of entries) {
479
+ const fullPath = path.join(currentPath, entry.name);
480
+ const relativePath = path.relative(dir, fullPath);
481
+ if (shouldIgnoreFile(relativePath, ignorePatterns)) {
482
+ continue;
483
+ }
484
+ if (entry.isDirectory()) {
485
+ await walk(fullPath);
486
+ }
487
+ else if (entry.isFile()) {
488
+ files.push(fullPath);
489
+ }
490
+ }
491
+ }
492
+ await walk(dir);
493
+ return files;
494
+ }
495
+ main().catch((error) => {
496
+ console.error(chalk.gray('\n✗ Fatal error:'), error.message);
497
+ process.exit(1);
498
+ });
@@ -0,0 +1,10 @@
1
+ interface Config {
2
+ email: string;
3
+ token: string;
4
+ apiKey: string;
5
+ expiryTime?: number;
6
+ }
7
+ export declare function getConfig(): Promise<Config | null>;
8
+ export declare function saveConfig(config: Config): Promise<void>;
9
+ export declare function clearConfig(): Promise<void>;
10
+ export {};
@@ -0,0 +1,49 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ const CONFIG_DIR = path.join(os.homedir(), '.optiq');
5
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'session.json');
6
+ export async function getConfig() {
7
+ try {
8
+ const data = await fs.readFile(CONFIG_FILE, 'utf-8');
9
+ const config = JSON.parse(data);
10
+ // Check if token is expired
11
+ if (config.expiryTime && Date.now() > config.expiryTime) {
12
+ await clearConfig();
13
+ return null;
14
+ }
15
+ return {
16
+ email: config.email,
17
+ token: config.token,
18
+ apiKey: config.contextEngineApiKey || config.apiKey,
19
+ };
20
+ }
21
+ catch (error) {
22
+ return null;
23
+ }
24
+ }
25
+ export async function saveConfig(config) {
26
+ try {
27
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
28
+ // Calculate expiry time (30 days from now)
29
+ const expiryTime = Date.now() + (30 * 24 * 60 * 60 * 1000);
30
+ const sessionData = {
31
+ token: config.token,
32
+ email: config.email,
33
+ expiryTime,
34
+ contextEngineApiKey: config.apiKey,
35
+ };
36
+ await fs.writeFile(CONFIG_FILE, JSON.stringify(sessionData, null, 2), 'utf-8');
37
+ }
38
+ catch (error) {
39
+ throw new Error('Failed to save config');
40
+ }
41
+ }
42
+ export async function clearConfig() {
43
+ try {
44
+ await fs.unlink(CONFIG_FILE);
45
+ }
46
+ catch (error) {
47
+ // Ignore if file doesn't exist
48
+ }
49
+ }
@@ -0,0 +1,7 @@
1
+ export declare function isValidDirectory(dirPath: string): Promise<{
2
+ valid: boolean;
3
+ error?: string;
4
+ fileCount?: number;
5
+ }>;
6
+ export declare function getGitIgnorePatterns(dir: string): Promise<string[]>;
7
+ export declare function shouldIgnoreFile(relativePath: string, patterns: string[]): boolean;
@@ -0,0 +1,103 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ const DEFAULT_IGNORE_PATTERNS = [
4
+ 'node_modules',
5
+ '.git',
6
+ 'dist',
7
+ 'build',
8
+ 'target',
9
+ '.next',
10
+ '.nuxt',
11
+ 'coverage',
12
+ '.cache',
13
+ '.vscode',
14
+ '.idea',
15
+ '*.log',
16
+ '.DS_Store',
17
+ 'Thumbs.db',
18
+ ];
19
+ export async function isValidDirectory(dirPath) {
20
+ try {
21
+ const stat = await fs.stat(dirPath);
22
+ if (!stat.isDirectory()) {
23
+ return { valid: false, error: 'Path is not a directory' };
24
+ }
25
+ // Count files
26
+ const fileCount = await countFiles(dirPath);
27
+ if (fileCount === 0) {
28
+ return { valid: false, error: 'Directory is empty' };
29
+ }
30
+ if (fileCount > 50000) {
31
+ return { valid: false, error: 'Directory has too many files (max: 50,000)' };
32
+ }
33
+ return { valid: true, fileCount };
34
+ }
35
+ catch (error) {
36
+ return { valid: false, error: 'Directory does not exist or is not accessible' };
37
+ }
38
+ }
39
+ async function countFiles(dir) {
40
+ let count = 0;
41
+ const ignorePatterns = await getGitIgnorePatterns(dir);
42
+ async function walk(currentPath) {
43
+ if (count > 50000)
44
+ return; // Stop counting if too many
45
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
46
+ for (const entry of entries) {
47
+ const fullPath = path.join(currentPath, entry.name);
48
+ const relativePath = path.relative(dir, fullPath);
49
+ if (shouldIgnoreFile(relativePath, ignorePatterns)) {
50
+ continue;
51
+ }
52
+ if (entry.isDirectory()) {
53
+ await walk(fullPath);
54
+ }
55
+ else if (entry.isFile()) {
56
+ count++;
57
+ }
58
+ }
59
+ }
60
+ try {
61
+ await walk(dir);
62
+ }
63
+ catch (error) {
64
+ // Ignore errors during counting
65
+ }
66
+ return count;
67
+ }
68
+ export async function getGitIgnorePatterns(dir) {
69
+ const patterns = [...DEFAULT_IGNORE_PATTERNS];
70
+ try {
71
+ const gitignorePath = path.join(dir, '.gitignore');
72
+ const content = await fs.readFile(gitignorePath, 'utf-8');
73
+ const lines = content.split('\n')
74
+ .map(line => line.trim())
75
+ .filter(line => line && !line.startsWith('#'));
76
+ patterns.push(...lines);
77
+ }
78
+ catch (error) {
79
+ // No .gitignore file, use defaults
80
+ }
81
+ return patterns;
82
+ }
83
+ export function shouldIgnoreFile(relativePath, patterns) {
84
+ const parts = relativePath.split(path.sep);
85
+ for (const pattern of patterns) {
86
+ // Exact match
87
+ if (parts.includes(pattern)) {
88
+ return true;
89
+ }
90
+ // Wildcard match
91
+ if (pattern.includes('*')) {
92
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
93
+ if (regex.test(relativePath)) {
94
+ return true;
95
+ }
96
+ }
97
+ // Extension match
98
+ if (pattern.startsWith('*.') && relativePath.endsWith(pattern.slice(1))) {
99
+ return true;
100
+ }
101
+ }
102
+ return false;
103
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@optiqcode/cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool for Optiq - automatic code indexing and context engine",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "optiq": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/index.js",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "keywords": [
17
+ "optiq",
18
+ "code-indexing",
19
+ "context-engine",
20
+ "cli",
21
+ "developer-tools"
22
+ ],
23
+ "author": "optiqcode",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/optiqcode/optiq"
28
+ },
29
+ "homepage": "https://github.com/optiqcode/optiq#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/optiqcode/optiq/issues"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ },
41
+ "dependencies": {
42
+ "axios": "^1.6.0",
43
+ "chalk": "^5.3.0",
44
+ "chokidar": "^3.5.3",
45
+ "log-update": "^7.0.1",
46
+ "ora": "^8.0.1",
47
+ "prompts": "^2.4.2"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^20.0.0",
51
+ "@types/prompts": "^2.4.9",
52
+ "typescript": "^5.3.0"
53
+ }
54
+ }