@coreviz/cli 1.0.1 → 1.0.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.
Files changed (3) hide show
  1. package/README.md +57 -4
  2. package/bin/cli.js +434 -16
  3. package/package.json +15 -2
package/README.md CHANGED
@@ -12,16 +12,69 @@ npm install -g @coreviz/cli
12
12
 
13
13
  ```bash
14
14
  # Run directly with npx
15
- npx @coreviz/cli
15
+ npx @coreviz/cli [command]
16
16
 
17
17
  # Or if installed globally
18
- coreviz
18
+ coreviz [command]
19
19
  ```
20
20
 
21
+ ## Commands
22
+
23
+ ### Authentication
24
+
25
+ Login to CoreViz using device authorization:
26
+
27
+ ```bash
28
+ coreviz login
29
+ ```
30
+
31
+ Logout:
32
+
33
+ ```bash
34
+ coreviz logout
35
+ ```
36
+
37
+ Check login status:
38
+
39
+ ```bash
40
+ coreviz whoami
41
+ ```
42
+
43
+ ### AI Features
44
+
45
+ Describe an image:
46
+
47
+ ```bash
48
+ coreviz describe path/to/image.jpg
49
+ ```
50
+
51
+ Edit an image with a text prompt:
52
+
53
+ ```bash
54
+ coreviz edit path/to/image.jpg --prompt "make it cyberpunk style"
55
+ ```
56
+
57
+ Search local images using natural language:
58
+
59
+ ```bash
60
+ coreviz search "a document with a red header"
61
+ ```
62
+
63
+ This will index the images in your current directory (creating a `.index.db` file) and return the top matches for your query.
64
+
21
65
  ## Development
22
66
 
23
- This is currently a placeholder CLI tool. The actual functionality will be implemented soon.
67
+ 1. Install dependencies:
68
+ ```bash
69
+ cd cli
70
+ npm install
71
+ ```
72
+
73
+ 2. Run local CLI:
74
+ ```bash
75
+ node bin/cli.js --help
76
+ ```
24
77
 
25
78
  ## License
26
79
 
27
- MIT
80
+ MIT
package/bin/cli.js CHANGED
@@ -1,18 +1,436 @@
1
1
  #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { createAuthClient } from "better-auth/client";
4
+ import { deviceAuthorizationClient } from "better-auth/client/plugins";
5
+ import open from 'open';
6
+ import Conf from 'conf';
7
+ import dotenv from 'dotenv';
8
+ import process from 'process';
9
+ import { intro, outro, confirm, isCancel, cancel, text } from '@clack/prompts';
10
+ import chalk from 'chalk';
11
+ import yoctoSpinner from 'yocto-spinner';
12
+ import { CoreViz } from '@coreviz/sdk';
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import Database from 'better-sqlite3';
2
16
 
3
- /**
4
- * CoreViz CLI
5
- *
6
- * This is a placeholder CLI tool for the @coreviz/cli package.
7
- * It can be executed via: npx @coreviz/cli
8
- */
9
-
10
- console.log('🚀 CoreViz CLI - Coming Soon!');
11
- console.log('');
12
- console.log('This is a placeholder for the CoreViz CLI tool.');
13
- console.log('The actual CLI functionality will be implemented soon.');
14
- console.log('');
15
- console.log('For more information, visit: https://coreviz.io/');
16
-
17
- // Exit with success code
18
- process.exit(0);
17
+ dotenv.config();
18
+
19
+ const config = new Conf({ projectName: 'coreviz-cli' });
20
+ const program = new Command();
21
+
22
+ const authClient = createAuthClient({
23
+ baseURL: "https://lab.coreviz.io",
24
+ plugins: [
25
+ deviceAuthorizationClient()
26
+ ]
27
+ });
28
+
29
+ program
30
+ .name('coreviz')
31
+ .description('CoreViz CLI')
32
+ .version('1.0.1');
33
+
34
+ program.command('login')
35
+ .description('Login to CoreViz using device authorization')
36
+ .action(async () => {
37
+ intro(chalk.bgHex('#663399').white('CoreViz'));
38
+
39
+ const session = config.get('session');
40
+ if (session) {
41
+ const shouldReauth = await confirm({
42
+ message: "You're already logged in. Do you want to log in again?",
43
+ initialValue: false,
44
+ });
45
+
46
+ if (isCancel(shouldReauth) || !shouldReauth) {
47
+ cancel("Login cancelled.");
48
+ process.exit(0);
49
+ }
50
+ }
51
+
52
+ const spinner = yoctoSpinner({ text: "Requesting device authorization..." });
53
+ spinner.start();
54
+
55
+ try {
56
+ const { data, error } = await authClient.device.code({
57
+ client_id: "coreviz-cli",
58
+ scope: "openid profile email",
59
+ });
60
+
61
+ spinner.stop();
62
+
63
+ if (error) {
64
+ cancel(`Failed to request device authorization: ${error.message || error}`);
65
+ process.exit(1);
66
+ }
67
+
68
+ if (!data) {
69
+ cancel('No data received from server.');
70
+ process.exit(1);
71
+ }
72
+
73
+ const { verification_uri, user_code, device_code, interval = 5, expires_in } = data;
74
+
75
+ console.log("");
76
+ console.log(chalk.cyan("📱 Device Authorization Required"));
77
+ console.log("");
78
+ console.log(`Please visit: ${chalk.underline.blue(verification_uri)}`);
79
+ console.log(`Enter code: ${chalk.bold.green(user_code)}`);
80
+ console.log("");
81
+
82
+ try {
83
+ await open(verification_uri);
84
+ } catch (err) {
85
+ console.log(chalk.yellow("Could not open browser automatically."));
86
+ }
87
+
88
+ console.log(chalk.gray(`Waiting for authorization (expires in ${Math.floor(expires_in / 60)} minutes)...`));
89
+
90
+ const tokenData = await pollForToken(device_code, interval);
91
+
92
+ if (tokenData) {
93
+ config.set('session', tokenData);
94
+
95
+ // Fetch user info to display name
96
+ const { data: sessionData } = await authClient.getSession({
97
+ fetchOptions: {
98
+ headers: {
99
+ Authorization: `Bearer ${tokenData.access_token}`,
100
+ },
101
+ },
102
+ });
103
+
104
+ outro(chalk.green(`✅ Login successful! Logged in as ${sessionData?.user?.name || sessionData?.user?.email || 'User'}`));
105
+ }
106
+
107
+ } catch (e) {
108
+ spinner.stop();
109
+ cancel(`An unexpected error occurred: ${e.message}`);
110
+ process.exit(1);
111
+ }
112
+ });
113
+
114
+ async function pollForToken(deviceCode, initialInterval) {
115
+ let pollingInterval = initialInterval;
116
+ const spinner = yoctoSpinner({ text: "Polling for authorization..." });
117
+ spinner.start();
118
+
119
+ return new Promise((resolve, reject) => {
120
+ const poll = async () => {
121
+ try {
122
+ const { data, error } = await authClient.device.token({
123
+ client_id: "coreviz-cli",
124
+ device_code: deviceCode,
125
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
126
+ });
127
+
128
+ if (data) {
129
+ spinner.stop();
130
+ resolve(data);
131
+ return;
132
+ } else if (error) {
133
+ switch (error.error) {
134
+ case "authorization_pending":
135
+ // Continue polling
136
+ break;
137
+ case "slow_down":
138
+ pollingInterval += 5;
139
+ spinner.text = chalk.yellow(`Slowing down polling to ${pollingInterval}s...`);
140
+ break;
141
+ case "access_denied":
142
+ spinner.stop();
143
+ cancel("Access was denied by the user.");
144
+ process.exit(1);
145
+ break;
146
+ case "expired_token":
147
+ spinner.stop();
148
+ cancel("The device code has expired. Please try again.");
149
+ process.exit(1);
150
+ break;
151
+ default:
152
+ // Ignore unknown errors and keep polling? Or fail?
153
+ // Better-auth might return other errors.
154
+ if (!['authorization_pending', 'slow_down'].includes(error.error)) {
155
+ spinner.stop();
156
+ cancel(`Error: ${error.error_description || error.message}`);
157
+ process.exit(1);
158
+ }
159
+ break;
160
+ }
161
+ }
162
+ } catch (err) {
163
+ spinner.stop();
164
+ cancel(`Network error: ${err.message}`);
165
+ process.exit(1);
166
+ }
167
+
168
+ setTimeout(poll, pollingInterval * 1000);
169
+ };
170
+
171
+ setTimeout(poll, pollingInterval * 1000);
172
+ });
173
+ }
174
+
175
+ program.command('logout')
176
+ .description('Logout')
177
+ .action(() => {
178
+ intro(chalk.bgHex('#663399').white('CoreViz'));
179
+ config.clear();
180
+ outro(chalk.green('Logged out successfully.'));
181
+ });
182
+
183
+ program.command('whoami')
184
+ .description('Show current user')
185
+ .action(() => {
186
+ intro(chalk.bgHex('#663399').white('CoreViz'));
187
+ const session = config.get('session');
188
+ if (session && (session.user || session.access_token)) {
189
+ const userDisplay = session.user
190
+ ? `${session.user.name} (${session.user.email})`
191
+ : 'Authenticated User (Token only)';
192
+ outro(chalk.green(`Logged in as: ${userDisplay}`));
193
+ } else {
194
+ outro(chalk.yellow('Not logged in.'));
195
+ }
196
+ });
197
+
198
+ program.command('edit <image-path>')
199
+ .description('Edit an image using AI')
200
+ .option('-p, --prompt <prompt>', 'Text description of the desired edit')
201
+ .action(async (imagePath, options) => {
202
+ intro(chalk.bgHex('#663399').white('CoreViz'));
203
+
204
+ const session = config.get('session');
205
+ if (!session || !session.access_token) {
206
+ cancel('You are not logged in. Please run `coreviz login` first.');
207
+ process.exit(1);
208
+ }
209
+
210
+ if (!fs.existsSync(imagePath)) {
211
+ cancel(`File not found: ${imagePath}`);
212
+ process.exit(1);
213
+ }
214
+
215
+ let prompt = options.prompt;
216
+ if (!prompt) {
217
+ prompt = await text({
218
+ message: 'What would you like to change in the image?',
219
+ placeholder: 'e.g., "Make it look like a painting" or "Add a red hat"',
220
+ validate(value) {
221
+ if (value.length === 0) return `Value is required!`;
222
+ },
223
+ });
224
+
225
+ if (isCancel(prompt)) {
226
+ cancel('Operation cancelled.');
227
+ process.exit(0);
228
+ }
229
+ }
230
+
231
+ const spinner = yoctoSpinner({ text: "Processing image..." });
232
+ spinner.start();
233
+
234
+ try {
235
+ const base64Image = readImageAsBase64(imagePath);
236
+
237
+ const coreviz = new CoreViz({ token: session.access_token });
238
+ const resultBase64 = await coreviz.edit(base64Image, {
239
+ prompt
240
+ });
241
+
242
+ spinner.stop();
243
+
244
+ // Save result
245
+ const outputFilename = `edited-${Date.now()}-${path.basename(imagePath)}`;
246
+ const outputBuffer = Buffer.from(resultBase64.replace(/^data:image\/\w+;base64,/, ""), 'base64');
247
+ fs.writeFileSync(outputFilename, outputBuffer);
248
+
249
+ outro(chalk.green(`✅ Image edited successfully! Saved as ${outputFilename}`));
250
+
251
+ } catch (error) {
252
+ spinner.stop();
253
+ cancel(`Failed to edit image: ${error.message}`);
254
+ process.exit(1);
255
+ }
256
+ });
257
+
258
+ program.command('describe <image-path>')
259
+ .description('Describe an image using AI')
260
+ .action(async (imagePath) => {
261
+ intro(chalk.bgHex('#663399').white('CoreViz'));
262
+
263
+ const session = config.get('session');
264
+ if (!session || !session.access_token) {
265
+ cancel('You are not logged in. Please run `coreviz login` first.');
266
+ process.exit(1);
267
+ }
268
+
269
+ if (!fs.existsSync(imagePath)) {
270
+ cancel(`File not found: ${imagePath}`);
271
+ process.exit(1);
272
+ }
273
+
274
+ const spinner = yoctoSpinner({ text: "Analyzing image..." });
275
+ spinner.start();
276
+
277
+ try {
278
+ const base64Image = readImageAsBase64(imagePath);
279
+ const coreviz = new CoreViz({ token: session.access_token });
280
+ const description = await coreviz.describe(base64Image);
281
+
282
+ spinner.stop();
283
+
284
+ outro(chalk.green('✅ Image description:'));
285
+ console.log(description);
286
+ } catch (error) {
287
+ spinner.stop();
288
+ if (error.message === 'Insufficient credits') {
289
+ cancel('Insufficient credits. Please add credits to your account.');
290
+ process.exit(1);
291
+ }
292
+ cancel(`Failed to describe image: ${error.message}`);
293
+ process.exit(1);
294
+ }
295
+ });
296
+
297
+ program.command('search <query>')
298
+ .description('Search for images in the current directory using AI')
299
+ .action(async (query) => {
300
+ intro(chalk.bgHex('#663399').white('CoreViz'));
301
+
302
+ const session = config.get('session');
303
+ if (!session || !session.access_token) {
304
+ cancel('You are not logged in. Please run `coreviz login` first.');
305
+ process.exit(1);
306
+ }
307
+
308
+ const spinner = yoctoSpinner({ text: "Indexing directory..." });
309
+ spinner.start();
310
+
311
+ const dbPath = path.join(process.cwd(), '.index.db');
312
+ const db = new Database(dbPath);
313
+
314
+ // Initialize DB
315
+ db.prepare(`
316
+ CREATE TABLE IF NOT EXISTS images (
317
+ path TEXT PRIMARY KEY,
318
+ mtime REAL,
319
+ embedding TEXT
320
+ )
321
+ `).run();
322
+
323
+ const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp', '.tiff'];
324
+ const files = fs.readdirSync(process.cwd())
325
+ .filter(file => imageExtensions.includes(path.extname(file).toLowerCase()));
326
+
327
+ if (files.length === 0) {
328
+ spinner.stop();
329
+ cancel('No images found in the current directory.');
330
+ process.exit(0);
331
+ }
332
+
333
+ const coreviz = new CoreViz({ token: session.access_token });
334
+
335
+ // Prepare statements
336
+ const getFile = db.prepare('SELECT mtime FROM images WHERE path = ?');
337
+ const upsertFile = db.prepare('INSERT OR REPLACE INTO images (path, mtime, embedding) VALUES (?, ?, ?)');
338
+ const deleteFile = db.prepare('DELETE FROM images WHERE path = ?');
339
+
340
+ // Clean up deleted files from index
341
+ const allIndexedFiles = db.prepare('SELECT path FROM images').all();
342
+ for (const row of allIndexedFiles) {
343
+ if (!files.includes(row.path)) {
344
+ deleteFile.run(row.path);
345
+ }
346
+ }
347
+
348
+ for (const file of files) {
349
+ const filePath = path.join(process.cwd(), file);
350
+ const stats = fs.statSync(filePath);
351
+ const mtime = stats.mtimeMs;
352
+
353
+ const existing = getFile.get(file);
354
+
355
+ // Skip if already indexed and not modified
356
+ if (existing && existing.mtime === mtime) {
357
+ continue;
358
+ }
359
+
360
+ spinner.text = `Indexing ${file}...`;
361
+
362
+ try {
363
+ const base64Image = readImageAsBase64(filePath);
364
+ const { embedding } = await coreviz.embed(base64Image, { type: 'image' });
365
+
366
+ upsertFile.run(file, mtime, JSON.stringify(embedding));
367
+ } catch (error) {
368
+ // Log error but continue
369
+ console.error(`Failed to index ${file}: ${error.message}`);
370
+ }
371
+ }
372
+
373
+ spinner.text = "Processing search query...";
374
+
375
+ try {
376
+ const { embedding: queryEmbedding } = await coreviz.embed(query, { type: 'text' });
377
+
378
+ const rows = db.prepare('SELECT path, embedding FROM images').all();
379
+ const results = [];
380
+
381
+ for (const row of rows) {
382
+ if (!row.embedding) continue;
383
+
384
+ const fileEmbedding = JSON.parse(row.embedding);
385
+
386
+ // Calculate cosine similarity
387
+ const similarity = cosineSimilarity(queryEmbedding, fileEmbedding);
388
+ results.push({ file: row.path, similarity });
389
+ }
390
+
391
+ // Sort by similarity descending
392
+ results.sort((a, b) => b.similarity - a.similarity);
393
+
394
+ spinner.stop();
395
+
396
+ outro(chalk.green(`✅ Search results for "${query}"`));
397
+
398
+ // Show top 5 results
399
+ results.slice(0, 5).forEach((result, i) => {
400
+ const score = (result.similarity * 100).toFixed(1);
401
+ console.log(`${i + 1}. ${chalk.bold(result.file)} ${chalk.gray(`(${score}%)`)}`);
402
+ });
403
+
404
+ } catch (error) {
405
+ spinner.stop();
406
+ cancel(`Search failed: ${error.message}`);
407
+ process.exit(1);
408
+ } finally {
409
+ db.close();
410
+ }
411
+ });
412
+
413
+ function cosineSimilarity(vecA, vecB) {
414
+ if (vecA.length !== vecB.length) return 0;
415
+
416
+ let dotProduct = 0;
417
+ let normA = 0;
418
+ let normB = 0;
419
+
420
+ for (let i = 0; i < vecA.length; i++) {
421
+ dotProduct += vecA[i] * vecB[i];
422
+ normA += vecA[i] * vecA[i];
423
+ normB += vecB[i] * vecB[i];
424
+ }
425
+
426
+ if (normA === 0 || normB === 0) return 0;
427
+
428
+ return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
429
+ }
430
+
431
+ function readImageAsBase64(imagePath) {
432
+ const imageBuffer = fs.readFileSync(imagePath);
433
+ return `data:image/${path.extname(imagePath).slice(1) || 'jpeg'};base64,${imageBuffer.toString('base64')}`;
434
+ }
435
+
436
+ program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@coreviz/cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
+ "type": "module",
4
5
  "description": "CoreViz CLI tool",
5
6
  "main": "index.js",
6
7
  "bin": {
@@ -23,5 +24,17 @@
23
24
  "bugs": {
24
25
  "url": "https://github.com/CoreViz/cli/issues"
25
26
  },
26
- "homepage": "https://github.com/CoreViz/cli#readme"
27
+ "homepage": "https://github.com/CoreViz/cli#readme",
28
+ "dependencies": {
29
+ "@clack/prompts": "^0.11.0",
30
+ "@coreviz/sdk": "^1.0.3",
31
+ "better-auth": "^1.4.2",
32
+ "better-sqlite3": "^12.4.6",
33
+ "chalk": "^5.6.2",
34
+ "commander": "^14.0.2",
35
+ "conf": "^15.0.2",
36
+ "dotenv": "^17.2.3",
37
+ "open": "^11.0.0",
38
+ "yocto-spinner": "^1.0.0"
39
+ }
27
40
  }