2
2
3
3
import fs from 'fs' ;
4
4
import path from 'path' ;
5
- import { TrieveSDK , Topic } from 'trieve-ts-sdk' ;
5
+ import { TrieveSDK , Topic , ChunkMetadata } from 'trieve-ts-sdk' ;
6
6
import { program } from 'commander' ;
7
7
import chalk from 'chalk' ;
8
8
import inquirer from 'inquirer' ;
9
9
import Conf from 'conf' ;
10
10
import os from 'os' ;
11
+ import readline from 'readline' ;
11
12
12
13
interface UploadedFile {
13
14
fileName: string ;
@@ -251,6 +252,11 @@ async function updateFileStatuses(): Promise<UploadedFile[]> {
251
252
}
252
253
}
253
254
255
+ // Sort files by uploadedAt timestamp (most recent first)
256
+ updatedFiles . sort ( ( a , b ) => {
257
+ return new Date ( b . uploadedAt ) . getTime ( ) - new Date ( a . uploadedAt ) . getTime ( ) ;
258
+ } ) ;
259
+
254
260
// Save the updated statuses
255
261
if ( updatedFiles . length > 0 ) {
256
262
fs . writeFileSync ( uploadedFilesPath , JSON . stringify ( updatedFiles , null , 2 ) ) ;
@@ -320,15 +326,22 @@ async function checkSpecificFile(trackingId: string): Promise<void> {
320
326
321
327
// Interactive function to select and check a specific file
322
328
async function interactiveCheckStatus ( ) : Promise < void > {
323
- const files = manageUploadedFiles ( 'get' ) ;
329
+ let files = manageUploadedFiles ( 'get' ) ;
324
330
325
331
if ( files . length === 0 ) {
326
332
console . log ( chalk . yellow ( 'No files have been uploaded yet.' ) ) ;
327
333
return ;
328
334
}
329
335
336
+ // Sort files by uploadedAt timestamp (most recent first)
337
+ files . sort ( ( a , b ) => {
338
+ return new Date ( b . uploadedAt ) . getTime ( ) - new Date ( a . uploadedAt ) . getTime ( ) ;
339
+ } ) ;
340
+
330
341
const fileChoices = files . map ( ( file ) => ( {
331
- name : `${ file . fileName } (${ file . trackingId } )` ,
342
+ name : `${ file . fileName } (${ file . trackingId } ) - ${ new Date (
343
+ file . uploadedAt ,
344
+ ) . toLocaleString ( ) } `,
332
345
value : file . trackingId ,
333
346
} ) ) ;
334
347
@@ -387,38 +400,134 @@ async function askQuestion(question: string): Promise<void> {
387
400
console . log ( chalk . blue ( '🔍 Fetching answer...' ) ) ;
388
401
389
402
// Create a message and stream the response
390
- const { reader, queryId } =
391
- await trieveClient . createMessageReaderWithQueryId ( {
392
- topic_id : topicData . id ,
393
- new_message_content : question ,
394
- } ) ;
395
-
396
- console . log ( chalk . yellow ( '\n🤖 Answer:' ) ) ;
397
- console . log ( '─' . repeat ( 80 ) ) ;
403
+ const { reader } = await trieveClient . createMessageReaderWithQueryId ( {
404
+ topic_id : topicData . id ,
405
+ new_message_content : question ,
406
+ use_agentic_search : true ,
407
+ } ) ;
398
408
399
409
// Stream the response
400
410
const decoder = new TextDecoder ( ) ;
401
- let answer = '' ;
411
+ let fullResponse = '' ;
412
+ let parsedChunks : ChunkMetadata [ ] = [ ] ;
413
+ let isCollapsed = true ;
414
+ let isChunkSection = true ; // Initially assume we're receiving chunks
415
+ let actualAnswer = '' ;
416
+
417
+ // Set up keyboard interaction for collapsible chunks
418
+ readline . emitKeypressEvents ( process . stdin ) ;
419
+ if ( process . stdin . isTTY ) {
420
+ process . stdin . setRawMode ( true ) ;
421
+ }
422
+
423
+ const keyPressHandler = (
424
+ str : string ,
425
+ key : { name : string ; ctrl ?: boolean ; sequence ?: string } ,
426
+ ) => {
427
+ // Handle both key.name and raw sequence for better compatibility
428
+ if ( key . name === 'j' || key . sequence === 'j' ) {
429
+ isCollapsed = ! isCollapsed ;
430
+ // Clear console and redisplay with updated collapse state
431
+ console . clear ( ) ;
432
+
433
+ if ( parsedChunks . length > 0 ) {
434
+ if ( isCollapsed ) {
435
+ console . log (
436
+ chalk . cyan (
437
+ `📚 Found ${ parsedChunks . length } reference chunks (press 'j' to expand)` ,
438
+ ) ,
439
+ ) ;
440
+ } else {
441
+ console . log ( formatChunksCollapsible ( parsedChunks ) ) ;
442
+ }
443
+
444
+ // Add a separator between chunks and answer
445
+ console . log ( chalk . dim ( '─' . repeat ( 40 ) + ' Answer ' + '─' . repeat ( 40 ) ) ) ;
446
+ }
447
+
448
+ if ( actualAnswer ) {
449
+ console . log ( actualAnswer ) ;
450
+ }
451
+ } else if ( key . name === 'c' && key . ctrl ) {
452
+ // Allow Ctrl+C to exit
453
+ process. exit ( ) ;
454
+ }
455
+ } ;
456
+
457
+ process . stdin . on ( 'keypress ', keyPressHandler ) ;
402
458
403
459
try {
404
460
while ( true ) {
405
461
const { done , value } = await reader . read ( ) ;
406
462
if ( done ) break ;
407
463
408
464
const chunk = decoder . decode ( value ) ;
409
- answer += chunk ;
410
- process . stdout . write ( chunk ) ;
465
+ fullResponse += chunk ;
466
+
467
+ // Check if we've reached the separator between chunks and answer
468
+ if ( isChunkSection && fullResponse . includes ( '||' ) ) {
469
+ isChunkSection = false ;
470
+ const parts = fullResponse . split ( '||' ) ;
471
+
472
+ try {
473
+ // The first part should contain the JSON array of chunks
474
+ const chunksJson = parts [ 0 ] . trim ( ) ;
475
+ if ( chunksJson ) {
476
+ parsedChunks = JSON . parse ( chunksJson ) ;
477
+
478
+ // Only show a collapsed summary initially
479
+ if ( isCollapsed ) {
480
+ console . log (
481
+ chalk . cyan (
482
+ `📚 Found ${ parsedChunks . length } reference chunks (press 'j' to expand)` ,
483
+ ) ,
484
+ ) ;
485
+ } else {
486
+ console . log ( formatChunksCollapsible ( parsedChunks ) ) ;
487
+ }
488
+
489
+ // Add a separator between chunks and answer
490
+ console . log (
491
+ chalk . dim ( '─' . repeat ( 40 ) + ' Answer ' + '─' . repeat ( 40 ) ) ,
492
+ ) ;
493
+ }
494
+ } catch ( e ) {
495
+ console . error ( chalk . red ( '❌ Error parsing chunks:' ) , e ) ;
496
+ }
497
+
498
+ // Start displaying the actual answer from the second part
499
+ actualAnswer = parts [ 1 ] || '' ;
500
+ process . stdout . write ( actualAnswer ) ;
501
+ } else if ( ! isChunkSection ) {
502
+ // We're in the answer section, just display the chunk
503
+ actualAnswer + = chunk ;
504
+ process . stdout . write ( chunk ) ;
505
+ }
411
506
}
412
507
} catch ( e ) {
413
508
console . error ( chalk . red ( '❌ Error streaming response:' ) , e ) ;
414
509
} finally {
415
510
reader. releaseLock ( ) ;
511
+ // Clean up the keypress listener
512
+ if ( process . stdin . isTTY ) {
513
+ process . stdin . setRawMode ( false ) ;
514
+ }
515
+ process . stdin . removeListener ( 'keypress' , keyPressHandler ) ;
416
516
}
417
517
418
518
console . log ( '\n' + '─' . repeat ( 80 ) ) ;
419
519
console . log ( chalk . green ( '✅ Response complete' ) ) ;
520
+
521
+ if ( parsedChunks . length > 0 ) {
522
+ console . log (
523
+ chalk . blue (
524
+ `📚 ${ parsedChunks . length } reference chunks used (press 'j' to ${ isCollapsed ? 'expand' : 'collapse' } )` ,
525
+ ) ,
526
+ ) ;
527
+ }
528
+
420
529
console . log (
421
- chalk . blue ( `📚 Topic ID: ${ topicData . id } (saved for future reference)` ) ,
530
+ chalk . blue ( `� Topic ID: ${ topicData . id } (saved for future reference)` ) ,
422
531
) ;
423
532
} catch ( error ) {
424
533
console . error (
@@ -428,6 +537,52 @@ async function askQuestion(question: string): Promise<void> {
428
537
}
429
538
}
430
539
540
+ // Function to format chunk metadata in a collapsible way
541
+ function formatChunksCollapsible ( chunks : ChunkMetadata [ ] ) : string {
542
+ if ( ! chunks || chunks . length === 0 ) {
543
+ return '' ;
544
+ }
545
+
546
+ const summary = chalk . cyan ( `📚 Found ${ chunks . length } reference chunks` ) ;
547
+ const collapsedMessage = chalk . dim ( `(Use 'j' to expand/collapse references)` ) ;
548
+
549
+ // Format each chunk in a more readable way
550
+ const formattedChunks = chunks
551
+ . map ( ( chunk , index ) => {
552
+ const header = chalk . yellow (
553
+ `\n📄 Reference #${ index + 1 } : ${ chunk . tracking_id || chunk . id . substring ( 0 , 8 ) } ` ,
554
+ ) ;
555
+
556
+ // Extract important fields for preview
557
+ const details = [
558
+ chunk . link ? chalk . blue ( `🔗 ${ chunk . link } ` ) : '' ,
559
+ chunk . tag_set ?. length
560
+ ? chalk . magenta ( `🏷️ Tags: ${ chunk . tag_set . join ( ', ' ) } ` )
561
+ : '' ,
562
+ chalk . grey (
563
+ `📅 Created: ${ new Date ( chunk . created_at ) . toLocaleString ( ) } ` ,
564
+ ) ,
565
+ ]
566
+ . filter ( Boolean )
567
+ . join ( '\n ' ) ;
568
+
569
+ // Create preview of chunk content (if available)
570
+ let contentPreview = '' ;
571
+ if ( chunk . chunk_html ) {
572
+ // Strip HTML tags for clean preview and limit length
573
+ const plainText = chunk . chunk_html . replace ( / < [ ^ > ] * > ? / gm, '' ) ;
574
+ contentPreview = chalk . white (
575
+ `\n "${ plainText . substring ( 0 , 150 ) } ${ plainText . length > 150 ? '...' : '' } "` ,
576
+ ) ;
577
+ }
578
+
579
+ return `${ header } \n ${ details } ${ contentPreview } ` ;
580
+ } )
581
+ . join ( '\n' ) ;
582
+
583
+ return `${ summary } ${ collapsedMessage } \n${ formattedChunks } ` ;
584
+ }
585
+
431
586
program
432
587
. name ( 'trieve-cli' )
433
588
. description ( 'A CLI tool for using Trieve' )
@@ -483,7 +638,6 @@ program
483
638
'-t, --tracking-id <trackingId>' ,
484
639
'Check specific file by tracking ID' ,
485
640
)
486
- . option ( '-a, --all' , 'Check all uploaded files' )
487
641
. action ( async ( options ) => {
488
642
if ( options . trackingId ) {
489
643
await checkSpecificFile ( options . trackingId ) ;
@@ -496,42 +650,41 @@ program
496
650
497
651
program
498
652
. command ( 'ask' )
499
- . description ( 'Ask a question and get a streamed response' )
500
- . argument ( '<question>' , 'The question to ask' )
653
+ . description (
654
+ 'Ask a question and get a streamed response (interactive mode if no question provided)' ,
655
+ )
656
+ . argument ( '[question]' , 'The question to ask' )
501
657
. action ( async ( question ) => {
502
- await askQuestion ( question ) ;
503
- } ) ;
504
-
505
- program
506
- . command ( 'interactive-ask' )
507
- . description ( 'Ask a question interactively and get a streamed response' )
508
- . action ( async ( ) => {
509
- const answers = await inquirer . prompt ( [
510
- {
511
- type : 'input' ,
512
- name : 'question' ,
513
- message : 'What would you like to ask?' ,
514
- validate : ( input ) => {
515
- if ( ! input ) return 'Question is required' ;
516
- return true ;
658
+ if ( question ) {
659
+ await askQuestion ( question ) ;
660
+ } else {
661
+ // Interactive mode when no question is provided
662
+ const answers = await inquirer . prompt ( [
663
+ {
664
+ type : 'input' ,
665
+ name : 'question' ,
666
+ message : 'What would you like to ask?' ,
667
+ validate : ( input ) => {
668
+ if ( ! input ) return 'Question is required' ;
669
+ return true ;
670
+ } ,
517
671
} ,
518
- } ,
519
- ] ) ;
672
+ ] ) ;
520
673
521
- await askQuestion ( answers . question ) ;
674
+ await askQuestion ( answers . question ) ;
675
+ }
522
676
} ) ;
523
677
524
678
program . addHelpText (
525
679
'after' ,
526
680
`
527
681
${ chalk . yellow ( 'Examples:' ) }
528
- $ ${ chalk . green ( 'trieve-cli upload path/to/file.txt -t my-tracking-id' ) }
529
682
$ ${ chalk . green ( 'trieve-cli configure' ) }
683
+ $ ${ chalk . green ( 'trieve-cli upload path/to/file.txt -t my-tracking-id' ) }
530
684
$ ${ chalk . green ( 'trieve-cli check-upload-status' ) }
531
- $ ${ chalk . green ( 'trieve-cli check-upload-status --all' ) }
532
685
$ ${ chalk . green ( 'trieve-cli check-upload-status --tracking-id <tracking-id>' ) }
533
686
$ ${ chalk . green ( 'trieve-cli ask "What is the capital of France?"' ) }
534
- $ ${ chalk . green ( 'trieve-cli interactive- ask' ) }
687
+ $ ${ chalk . green ( 'trieve-cli ask' ) }
535
688
` ,
536
689
) ;
537
690
0 commit comments