@coreviz/cli 1.0.7 → 1.0.9

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/README.md CHANGED
@@ -52,7 +52,7 @@ npx @coreviz/cli describe path/to/image.jpg
52
52
 
53
53
 
54
54
 
55
- Edit an image with a text prompt:
55
+ Edit an image with a text prompt (🍌 Nano Banan + Flux Kontext in the CLI!):
56
56
 
57
57
  ```bash
58
58
  npx @coreviz/cli edit path/to/image.jpg --prompt "make it cyberpunk style"
@@ -66,6 +66,9 @@ Search local images using natural language:
66
66
  npx @coreviz/cli search "a person wearing a red t-shirt"
67
67
  ```
68
68
 
69
+ ![Screenshot of CoreViz CLI visually searching through a folder using AI.](./screenshots/search.png)
70
+
71
+
69
72
  This will index the images in your current directory (creating a `.index.db` file) and return the top matches for your query.
70
73
 
71
74
  ## Development
package/bin/cli.js CHANGED
@@ -195,25 +195,38 @@ program.command('whoami')
195
195
  }
196
196
  });
197
197
 
198
- program.command('edit <image-path>')
198
+ program.command('edit <image-path> <prompt>')
199
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'));
200
+ .option('--quiet', 'Suppress UI output (for scripting)')
201
+ .action(async (imagePath, prompt, options) => {
202
+ if (!options.quiet) {
203
+ intro(chalk.bgHex('#663399').white('CoreViz'));
204
+ }
203
205
 
204
206
  const session = config.get('session');
205
207
  if (!session || !session.access_token) {
208
+ if (options.quiet) {
209
+ console.error('Not logged in.');
210
+ process.exit(1);
211
+ }
206
212
  cancel('You are not logged in. Please run `coreviz login` first.');
207
213
  process.exit(1);
208
214
  }
209
215
 
210
216
  if (!fs.existsSync(imagePath)) {
217
+ if (options.quiet) {
218
+ console.error(`File not found: ${imagePath}`);
219
+ process.exit(1);
220
+ }
211
221
  cancel(`File not found: ${imagePath}`);
212
222
  process.exit(1);
213
223
  }
214
224
 
215
- let prompt = options.prompt;
216
225
  if (!prompt) {
226
+ if (options.quiet) {
227
+ console.error('Prompt is required in quiet mode.');
228
+ process.exit(1);
229
+ }
217
230
  prompt = await text({
218
231
  message: 'What would you like to change in the image?',
219
232
  placeholder: 'e.g., "Make it look like a painting" or "Add a red hat"',
@@ -228,8 +241,11 @@ program.command('edit <image-path>')
228
241
  }
229
242
  }
230
243
 
231
- const spinner = yoctoSpinner({ text: "Processing image..." });
232
- spinner.start();
244
+ let spinner;
245
+ if (!options.quiet) {
246
+ spinner = yoctoSpinner({ text: "Processing image..." });
247
+ spinner.start();
248
+ }
233
249
 
234
250
  try {
235
251
  const base64Image = readImageAsBase64(imagePath);
@@ -239,74 +255,229 @@ program.command('edit <image-path>')
239
255
  prompt
240
256
  });
241
257
 
242
- spinner.stop();
258
+ if (spinner) spinner.stop();
243
259
 
244
260
  // Save result
245
261
  const outputFilename = `edited-${Date.now()}-${path.basename(imagePath)}`;
246
262
  const outputBuffer = Buffer.from(resultBase64.replace(/^data:image\/\w+;base64,/, ""), 'base64');
247
263
  fs.writeFileSync(outputFilename, outputBuffer);
248
264
 
249
- outro(chalk.green(`✅ Image edited successfully! Saved as ${outputFilename}`));
265
+ if (options.quiet) {
266
+ console.log(outputFilename);
267
+ } else {
268
+ outro(chalk.green(`✅ Image edited successfully! Saved as ${outputFilename}`));
269
+ }
250
270
 
251
271
  } catch (error) {
252
- spinner.stop();
253
- cancel(`Failed to edit image: ${error.message}`);
272
+ if (spinner) spinner.stop();
273
+ const msg = error.message.includes('credits')
274
+ ? 'Insufficient credits. Please add credits to your account on https://lab.coreviz.io.'
275
+ : `Failed to edit image: ${error.message}`;
276
+ if (options.quiet) {
277
+ console.error(msg);
278
+ } else {
279
+ cancel(msg);
280
+ }
254
281
  process.exit(1);
255
282
  }
256
283
  });
257
284
 
258
285
  program.command('describe <image-path>')
259
286
  .description('Describe an image using AI')
260
- .action(async (imagePath) => {
261
- intro(chalk.bgHex('#663399').white('CoreViz'));
287
+ .option('--quiet', 'Suppress UI output (for scripting)')
288
+ .action(async (imagePath, options) => {
289
+ if (!options.quiet) {
290
+ intro(chalk.bgHex('#663399').white('CoreViz'));
291
+ }
262
292
 
263
293
  const session = config.get('session');
264
294
  if (!session || !session.access_token) {
295
+ if (options.quiet) {
296
+ console.error('Not logged in.');
297
+ process.exit(1);
298
+ }
265
299
  cancel('You are not logged in. Please run `coreviz login` first.');
266
300
  process.exit(1);
267
301
  }
268
302
 
269
303
  if (!fs.existsSync(imagePath)) {
304
+ if (options.quiet) {
305
+ console.error(`File not found: ${imagePath}`);
306
+ process.exit(1);
307
+ }
270
308
  cancel(`File not found: ${imagePath}`);
271
309
  process.exit(1);
272
310
  }
273
311
 
274
- const spinner = yoctoSpinner({ text: "Analyzing image..." });
275
- spinner.start();
312
+ let spinner;
313
+ if (!options.quiet) {
314
+ spinner = yoctoSpinner({ text: "Analyzing image..." });
315
+ spinner.start();
316
+ }
276
317
 
277
318
  try {
278
319
  const base64Image = readImageAsBase64(imagePath);
279
320
  const coreviz = new CoreViz({ token: session.access_token });
280
321
  const description = await coreviz.describe(base64Image);
281
322
 
282
- spinner.stop();
323
+ if (spinner) spinner.stop();
283
324
 
284
- outro(chalk.green('✅ Image description:'));
285
- console.log(description);
325
+ if (options.quiet) {
326
+ console.log(description);
327
+ } else {
328
+ outro(chalk.green('✅ Image description:'));
329
+ console.log(description);
330
+ }
286
331
  } catch (error) {
287
- spinner.stop();
288
- if (error.message === 'Insufficient credits') {
289
- cancel('Insufficient credits. Please add credits to your account.');
332
+ if (spinner) spinner.stop();
333
+ const msg = error.message.includes('credits')
334
+ ? 'Insufficient credits. Please add credits to your account on https://lab.coreviz.io.'
335
+ : `Failed to describe image: ${error.message}`;
336
+
337
+ if (options.quiet) {
338
+ console.error(msg);
339
+ } else {
340
+ cancel(msg);
341
+ }
342
+ process.exit(1);
343
+ }
344
+ });
345
+
346
+ program.command('tag <image-path> <prompt>')
347
+ .description('Generate tags for an image using AI')
348
+ .option('--choices <items>', 'Comma-separated list of possible tags to choose from (optional)', '')
349
+ .option('--single', 'Return only one tag', false)
350
+ .option('-m, --mode <mode>', 'The mode to use for tagging. Defaults to "api".', 'api')
351
+ .option('--quiet', 'Output raw text for scripting (suppresses UI)')
352
+ .action(async (imagePath, prompt, options) => {
353
+ if (!options.quiet) {
354
+ intro(chalk.bgHex('#663399').white('CoreViz'));
355
+ }
356
+
357
+ const session = config.get('session');
358
+ if (!session || !session.access_token) {
359
+ if (options.quiet) {
360
+ console.error('Not logged in.');
290
361
  process.exit(1);
291
362
  }
292
- cancel(`Failed to describe image: ${error.message}`);
363
+ cancel('You are not logged in. Please run `coreviz login` first.');
364
+ process.exit(1);
365
+ }
366
+
367
+ if (!fs.existsSync(imagePath)) {
368
+ if (options.quiet) {
369
+ console.error(`File not found: ${imagePath}`);
370
+ process.exit(1);
371
+ }
372
+ cancel(`File not found: ${imagePath}`);
373
+ process.exit(1);
374
+ }
375
+
376
+ let tagList = options.choices ? options.choices.split(',').map(s => s.trim()) : undefined;
377
+
378
+ if (!prompt) {
379
+ if (tagList && tagList.length > 0) {
380
+ prompt = "Select the best matching tags";
381
+ } else {
382
+ if (options.quiet) {
383
+ console.error('Prompt is required in quiet mode.');
384
+ process.exit(1);
385
+ }
386
+ prompt = await text({
387
+ message: 'What kind of tags do you want to generate?',
388
+ placeholder: 'e.g., "jersey number of the player", "color of the car", etc.',
389
+ validate(value) {
390
+ if (value.length === 0) return `Value is required!`;
391
+ },
392
+ });
393
+
394
+ if (isCancel(prompt)) {
395
+ cancel('Operation cancelled.');
396
+ process.exit(0);
397
+ }
398
+ }
399
+ }
400
+
401
+ let spinner;
402
+ if (!options.quiet) {
403
+ setTimeout(() => {
404
+ if (spinner.isSpinning && options.mode === 'local') {
405
+ spinner.text = "On the first run, it might take a few minutes to load the local model, please wait...";
406
+ } else if (spinner.isSpinning && options.mode === 'api') {
407
+ spinner.text = "This might take a few seconds...";
408
+ }
409
+ }, 8000);
410
+ spinner = yoctoSpinner({ text: "Generating tags..." });
411
+ spinner.start();
412
+ }
413
+
414
+ try {
415
+ const base64Image = readImageAsBase64(imagePath);
416
+ const coreviz = new CoreViz({ token: session.access_token });
417
+
418
+ const response = await coreviz.tag(base64Image, {
419
+ mode: options.mode,
420
+ prompt,
421
+ options: tagList,
422
+ multiple: !options.single
423
+ });
424
+
425
+ if (spinner) spinner.stop();
426
+
427
+ if (options.quiet) {
428
+ if (response.tags && response.tags.length > 0) {
429
+ console.log(response.tags.join('\n'));
430
+ }
431
+ } else {
432
+ if (response.tags && response.tags.length > 0) {
433
+ outro(chalk.green('✅ Tags generated:'));
434
+ response.tags.forEach(tag => console.log(chalk.blue(`• ${tag}`)));
435
+ } else {
436
+ outro(chalk.yellow('No tags generated.'));
437
+ }
438
+ }
439
+
440
+ } catch (error) {
441
+ if (spinner) spinner.stop();
442
+ const msg = error.message.includes('credits')
443
+ ? 'Insufficient credits. Please add credits to your account on https://lab.coreviz.io.'
444
+ : `Failed to generate tags: ${error.message}`;
445
+
446
+ if (options.quiet) {
447
+ console.error(msg);
448
+ } else {
449
+ cancel(msg);
450
+ }
293
451
  process.exit(1);
294
452
  }
295
453
  });
296
454
 
297
455
  program.command('search <query>')
298
456
  .description('Search for images in the current directory using AI')
299
- .action(async (query) => {
300
- intro(chalk.bgHex('#663399').white('CoreViz'));
457
+ .option('-m, --mode <mode>', 'The mode to use for embedding. Defaults to "local".', 'local')
458
+ .option('--quiet', 'Suppress UI output (for scripting)')
459
+ .action(async (query, options) => {
460
+ if (!options.quiet) {
461
+ intro(chalk.bgHex('#663399').white('CoreViz'));
462
+ }
463
+
464
+ const mode = options.mode || 'local';
301
465
 
302
466
  const session = config.get('session');
303
467
  if (!session || !session.access_token) {
468
+ if (options.quiet) {
469
+ console.error('Not logged in.');
470
+ process.exit(1);
471
+ }
304
472
  cancel('You are not logged in. Please run `coreviz login` first.');
305
473
  process.exit(1);
306
474
  }
307
475
 
308
- const spinner = yoctoSpinner({ text: "Indexing directory..." });
309
- spinner.start();
476
+ let spinner;
477
+ if (!options.quiet) {
478
+ spinner = yoctoSpinner({ text: "Indexing directory..." });
479
+ spinner.start();
480
+ }
310
481
 
311
482
  const dbPath = path.join(process.cwd(), '.index.db');
312
483
  const db = new Database(dbPath);
@@ -325,7 +496,12 @@ program.command('search <query>')
325
496
  .filter(file => imageExtensions.includes(path.extname(file).toLowerCase()));
326
497
 
327
498
  if (files.length === 0) {
328
- spinner.stop();
499
+ if (spinner) spinner.stop();
500
+ if (options.quiet) {
501
+ // No images found, just exit with 0 (empty result) or 1?
502
+ // Usually empty search is exit 0 with empty stdout.
503
+ process.exit(0);
504
+ }
329
505
  cancel('No images found in the current directory.');
330
506
  process.exit(0);
331
507
  }
@@ -345,6 +521,17 @@ program.command('search <query>')
345
521
  }
346
522
  }
347
523
 
524
+ if (mode === 'local') {
525
+ // You're using the local model, it might take a few minutes for the model to load on the first run.
526
+ setTimeout(() => {
527
+ if (spinner.isSpinning && mode === 'local') {
528
+ spinner.text = "On the first run, it might take a few minutes to load the local model, please wait...";
529
+ }
530
+ }, 8000);
531
+ await coreviz.embed('text', { type: 'text', mode: mode });
532
+ if (spinner) spinner.stop();
533
+ }
534
+
348
535
  for (const file of files) {
349
536
  const filePath = path.join(process.cwd(), file);
350
537
  const stats = fs.statSync(filePath);
@@ -357,23 +544,29 @@ program.command('search <query>')
357
544
  continue;
358
545
  }
359
546
 
360
- spinner.text = `Indexing ${file}...`;
547
+ if (spinner) spinner.text = `Indexing ${file}...`;
361
548
 
362
549
  try {
363
550
  const base64Image = readImageAsBase64(filePath);
364
- const { embedding } = await coreviz.embed(base64Image, { type: 'image', mode: 'local' });
551
+ const { embedding } = await coreviz.embed(base64Image, { type: 'image', mode: mode });
365
552
 
366
553
  upsertFile.run(file, mtime, JSON.stringify(embedding));
367
554
  } catch (error) {
368
555
  // Log error but continue
369
- console.error(`Failed to index ${file}: ${error.message}`);
556
+ if (!options.quiet) {
557
+ if (error.message.includes('credits')) {
558
+ cancel('Insufficient credits. Please add credits to your account on https://lab.coreviz.io.');
559
+ process.exit(1);
560
+ }
561
+ console.error(`Failed to index ${file}: ${error.message}`);
562
+ }
370
563
  }
371
564
  }
372
565
 
373
- spinner.text = "Processing search query...";
566
+ if (spinner) spinner.text = "Processing search query...";
374
567
 
375
568
  try {
376
- const { embedding: queryEmbedding } = await coreviz.embed(query, { type: 'text', mode: 'local' });
569
+ const { embedding: queryEmbedding } = await coreviz.embed(query, { type: 'text', mode: mode });
377
570
 
378
571
  const rows = db.prepare('SELECT path, embedding FROM images').all();
379
572
  const results = [];
@@ -384,50 +577,44 @@ program.command('search <query>')
384
577
  const fileEmbedding = JSON.parse(row.embedding);
385
578
 
386
579
  // Calculate cosine similarity
387
- const similarity = cosineSimilarity(queryEmbedding, fileEmbedding);
580
+ const similarity = coreviz.similarity(queryEmbedding, fileEmbedding);
388
581
  results.push({ file: row.path, similarity });
389
582
  }
390
583
 
391
584
  // Sort by similarity descending
392
585
  results.sort((a, b) => b.similarity - a.similarity);
393
586
 
394
- spinner.stop();
587
+ if (spinner) spinner.stop();
395
588
 
396
- outro(chalk.green(`✅ Search results for "${query}"`));
589
+ if (options.quiet) {
590
+ // Output raw file paths (top 5)
591
+ results.slice(0, 5).forEach(result => {
592
+ console.log(result.file);
593
+ });
594
+ } else {
595
+ outro(chalk.green(`✅ Search results for "${query}"`));
397
596
 
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
- });
597
+ // Show top 5 results
598
+ results.slice(0, 5).forEach((result, i) => {
599
+ const score = (result.similarity * 100).toFixed(1);
600
+ console.log(`${i + 1}. ${chalk.bold(result.file)} ${chalk.gray(`(${score}%)`)}`);
601
+ });
602
+ }
403
603
 
404
604
  } catch (error) {
405
- spinner.stop();
406
- cancel(`Search failed: ${error.message}`);
605
+ if (spinner) spinner.stop();
606
+ const msg = `Search failed: ${error.message}`;
607
+ if (options.quiet) {
608
+ console.error(msg);
609
+ } else {
610
+ cancel(msg);
611
+ }
407
612
  process.exit(1);
408
613
  } finally {
409
614
  db.close();
410
615
  }
411
616
  });
412
617
 
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
618
  function readImageAsBase64(imagePath) {
432
619
  const imageBuffer = fs.readFileSync(imagePath);
433
620
  return `data:image/${path.extname(imagePath).slice(1) || 'jpeg'};base64,${imageBuffer.toString('base64')}`;
@@ -0,0 +1,13 @@
1
+ #!/bin/bash
2
+
3
+ # Loop through all image files
4
+ for file in *.jpg *.png *.jpeg *.webp; do
5
+ [ -e "$file" ] || continue
6
+
7
+ echo "Processing $file..."
8
+
9
+ # Extract jersey number using coreviz tag with --quiet
10
+ jersey_number=$(npx @coreviz/cli edit "$file" --prompt "make it cyberpunk style")
11
+
12
+ echo " Edited image: $edited_image"
13
+ done
File without changes
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+
3
+ # Loop through all image files
4
+ for file in *.jpg *.png *.jpeg *.webp; do
5
+ [ -e "$file" ] || continue
6
+
7
+ echo "Processing $file..."
8
+
9
+ # Extract jersey number using coreviz tag with --quiet
10
+ jersey_number=$(npx @coreviz/cli tag "$file" "What is the player's jersey number? Return only the number." \
11
+ --single \
12
+ --mode local \
13
+ --quiet)
14
+
15
+ if [ -n "$jersey_number" ]; then
16
+ echo " Found jersey number: $jersey_number"
17
+ mkdir -p "player_$jersey_number"
18
+ mv "$file" "player_$jersey_number/"
19
+ else
20
+ echo " Could not detect jersey number for $file"
21
+ fi
22
+ done
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coreviz/cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "type": "module",
5
5
  "description": "CoreViz CLI tool",
6
6
  "main": "index.js",
@@ -27,7 +27,7 @@
27
27
  "homepage": "https://github.com/CoreViz/cli#readme",
28
28
  "dependencies": {
29
29
  "@clack/prompts": "^0.11.0",
30
- "@coreviz/sdk": "^1.0.8",
30
+ "@coreviz/sdk": "^1.0.10",
31
31
  "better-auth": "^1.4.2",
32
32
  "better-sqlite3": "^12.4.6",
33
33
  "chalk": "^5.6.2",
Binary file