@@ -87,6 +87,12 @@ enum Command {
8787 #[ command( subcommand) ]
8888 action : GraphAction ,
8989 } ,
90+
91+ /// Query vault context.
92+ Context {
93+ #[ command( subcommand) ]
94+ action : ContextAction ,
95+ } ,
9096}
9197
9298#[ derive( Subcommand , Debug ) ]
@@ -100,6 +106,47 @@ enum GraphAction {
100106 Stats ,
101107}
102108
109+ #[ derive( Subcommand , Debug ) ]
110+ enum ContextAction {
111+ /// Read a note's full content with metadata.
112+ Read {
113+ /// File path, basename, or #docid.
114+ file : String ,
115+ } ,
116+ /// List notes by metadata filters.
117+ List {
118+ /// Filter to folder path prefix.
119+ #[ arg( long) ]
120+ folder : Option < String > ,
121+ /// Filter to notes with all listed tags (comma-separated).
122+ #[ arg( long, value_delimiter = ',' ) ]
123+ tags : Vec < String > ,
124+ /// Maximum results.
125+ #[ arg( long, default_value = "20" ) ]
126+ limit : usize ,
127+ } ,
128+ /// Vault structure overview.
129+ VaultMap ,
130+ /// Person context bundle.
131+ Who {
132+ /// Person name (matches filename in People folder).
133+ name : String ,
134+ } ,
135+ /// Project context bundle.
136+ Project {
137+ /// Project name (matches filename).
138+ name : String ,
139+ } ,
140+ /// Rich topic context with budget.
141+ Topic {
142+ /// Search query for the topic.
143+ query : String ,
144+ /// Character budget (default 32000, ~8000 tokens).
145+ #[ arg( long, default_value = "32000" ) ]
146+ budget : usize ,
147+ } ,
148+ }
149+
103150#[ derive( Subcommand , Debug ) ]
104151enum ModelsAction {
105152 /// List available models.
@@ -460,6 +507,215 @@ fn main() -> Result<()> {
460507 }
461508 }
462509
510+ Command :: Context { action } => {
511+ if !index_exists ( & data_dir) {
512+ eprintln ! ( "No index found. Run 'engraph index <path>' first." ) ;
513+ std:: process:: exit ( 1 ) ;
514+ }
515+ let db_path = data_dir. join ( "engraph.db" ) ;
516+ let store = store:: Store :: open ( & db_path) ?;
517+ let vault_path_str = store. get_meta ( "vault_path" ) ?. ok_or_else ( || {
518+ anyhow:: anyhow!( "No vault path in index. Run 'engraph index <path>' first." )
519+ } ) ?;
520+ let vault_path = PathBuf :: from ( & vault_path_str) ;
521+ let profile = config:: Config :: load_vault_profile ( ) . ok ( ) . flatten ( ) ;
522+
523+ let params = engraph:: context:: ContextParams {
524+ store : & store,
525+ vault_path : & vault_path,
526+ profile : profile. as_ref ( ) ,
527+ } ;
528+
529+ match action {
530+ ContextAction :: Read { file } => {
531+ let note = engraph:: context:: context_read ( & params, & file) ?;
532+ if cli. json {
533+ println ! ( "{}" , serde_json:: to_string_pretty( & note) ?) ;
534+ } else {
535+ println ! (
536+ "{} {}" ,
537+ note. path,
538+ note. docid
539+ . as_deref( )
540+ . map( |d| format!( "(#{})" , d) )
541+ . unwrap_or_default( )
542+ ) ;
543+ println ! ( "Tags: {}" , note. tags. join( ", " ) ) ;
544+ println ! ( "Outgoing links: {}" , note. outgoing_links. len( ) ) ;
545+ println ! ( "Incoming links: {}" , note. incoming_links. len( ) ) ;
546+ println ! ( "Chars: {}\n " , note. char_count) ;
547+ println ! ( "{}" , note. body) ;
548+ }
549+ }
550+ ContextAction :: List {
551+ folder,
552+ tags,
553+ limit,
554+ } => {
555+ let items =
556+ engraph:: context:: context_list ( & params, folder. as_deref ( ) , & tags, limit) ?;
557+ if cli. json {
558+ println ! ( "{}" , serde_json:: to_string_pretty( & items) ?) ;
559+ } else {
560+ for item in & items {
561+ let did = item
562+ . docid
563+ . as_deref ( )
564+ . map ( |d| format ! ( " #{d}" ) )
565+ . unwrap_or_default ( ) ;
566+ let tags_str = if item. tags . is_empty ( ) {
567+ String :: new ( )
568+ } else {
569+ format ! ( " [{}]" , item. tags. join( ", " ) )
570+ } ;
571+ println ! (
572+ "{}{}{} ({} edges)" ,
573+ item. path, did, tags_str, item. edge_count
574+ ) ;
575+ }
576+ println ! ( "\n {} notes" , items. len( ) ) ;
577+ }
578+ }
579+ ContextAction :: VaultMap => {
580+ let map = engraph:: context:: vault_map ( & params) ?;
581+ if cli. json {
582+ println ! ( "{}" , serde_json:: to_string_pretty( & map) ?) ;
583+ } else {
584+ println ! ( "Vault: {}" , map. vault_path) ;
585+ println ! ( "Type: {}, Structure: {}" , map. vault_type, map. structure) ;
586+ println ! (
587+ "Files: {}, Chunks: {}, Edges: {}\n " ,
588+ map. total_files, map. total_chunks, map. total_edges
589+ ) ;
590+ println ! ( "Folders:" ) ;
591+ for f in & map. folders {
592+ println ! ( " {}: {} notes" , f. path, f. note_count) ;
593+ }
594+ println ! ( "\n Top tags:" ) ;
595+ for ( tag, count) in & map. top_tags {
596+ println ! ( " {}: {}" , tag, count) ;
597+ }
598+ println ! ( "\n Recent files:" ) ;
599+ for path in & map. recent_files {
600+ println ! ( " {}" , path) ;
601+ }
602+ }
603+ }
604+ ContextAction :: Who { name } => {
605+ let person = engraph:: context:: context_who ( & params, & name) ?;
606+ if cli. json {
607+ println ! ( "{}" , serde_json:: to_string_pretty( & person) ?) ;
608+ } else {
609+ println ! ( "# {}\n " , person. name) ;
610+ if let Some ( note) = & person. note {
611+ println ! (
612+ "Note: {} {}" ,
613+ note. path,
614+ note. docid
615+ . as_deref( )
616+ . map( |d| format!( "(#{})" , d) )
617+ . unwrap_or_default( )
618+ ) ;
619+ println ! ( "Tags: {}\n " , note. tags. join( ", " ) ) ;
620+ println ! ( "{}\n " , note. body) ;
621+ } else {
622+ println ! ( "(No person note found)\n " ) ;
623+ }
624+ if !person. mentioned_in . is_empty ( ) {
625+ println ! ( "Mentioned in ({} notes):" , person. mentioned_in. len( ) ) ;
626+ for m in & person. mentioned_in {
627+ println ! ( " {} — {}" , m. path, m. snippet) ;
628+ }
629+ println ! ( ) ;
630+ }
631+ if !person. linked_from . is_empty ( ) {
632+ println ! ( "Linked from ({}):" , person. linked_from. len( ) ) ;
633+ for p in & person. linked_from {
634+ println ! ( " {}" , p) ;
635+ }
636+ println ! ( ) ;
637+ }
638+ println ! ( "Total: {} chars" , person. total_chars) ;
639+ }
640+ }
641+ ContextAction :: Project { name } => {
642+ let proj = engraph:: context:: context_project ( & params, & name) ?;
643+ if cli. json {
644+ println ! ( "{}" , serde_json:: to_string_pretty( & proj) ?) ;
645+ } else {
646+ println ! ( "# {}\n " , proj. name) ;
647+ if let Some ( note) = & proj. note {
648+ println ! ( "Note: {}\n " , note. path) ;
649+ println ! ( "{}\n " , note. body) ;
650+ }
651+ if !proj. active_tasks . is_empty ( ) {
652+ println ! ( "Active tasks ({}):" , proj. active_tasks. len( ) ) ;
653+ for t in & proj. active_tasks {
654+ println ! ( " - [ ] {} ({})" , t. text, t. source_file) ;
655+ }
656+ println ! ( ) ;
657+ }
658+ if !proj. child_notes . is_empty ( ) {
659+ println ! ( "Child notes ({}):" , proj. child_notes. len( ) ) ;
660+ for c in & proj. child_notes {
661+ println ! ( " {}" , c. path) ;
662+ }
663+ println ! ( ) ;
664+ }
665+ if !proj. team . is_empty ( ) {
666+ println ! ( "Team:" ) ;
667+ for p in & proj. team {
668+ println ! ( " {}" , p) ;
669+ }
670+ println ! ( ) ;
671+ }
672+ if !proj. recent_mentions . is_empty ( ) {
673+ println ! ( "Recent daily mentions:" ) ;
674+ for m in & proj. recent_mentions {
675+ println ! ( " {} — {}" , m. path, m. snippet) ;
676+ }
677+ println ! ( ) ;
678+ }
679+ }
680+ }
681+ ContextAction :: Topic { query, budget } => {
682+ let models_dir = data_dir. join ( "models" ) ;
683+ let mut embedder = engraph:: embedder:: Embedder :: new ( & models_dir) ?;
684+ let hnsw_dir = data_dir. join ( "hnsw" ) ;
685+ let index = engraph:: hnsw:: HnswIndex :: load ( & hnsw_dir) ?;
686+
687+ let bundle = engraph:: context:: context_topic_with_search (
688+ & params,
689+ & query,
690+ budget,
691+ & mut embedder,
692+ & index,
693+ ) ?;
694+ if cli. json {
695+ println ! ( "{}" , serde_json:: to_string_pretty( & bundle) ?) ;
696+ } else {
697+ println ! ( "# Context: {}\n " , bundle. topic) ;
698+ println ! (
699+ "Budget: {} / {} chars{}\n " ,
700+ bundle. total_chars,
701+ bundle. budget_chars,
702+ if bundle. truncated { " (truncated)" } else { "" }
703+ ) ;
704+ for s in & bundle. sections {
705+ let did = s
706+ . docid
707+ . as_deref ( )
708+ . map ( |d| format ! ( " #{d}" ) )
709+ . unwrap_or_default ( ) ;
710+ println ! ( "## {} — {}{}" , s. label, s. path, did) ;
711+ println ! ( "[{}]\n " , s. relevance) ;
712+ println ! ( "{}\n " , s. content) ;
713+ }
714+ }
715+ }
716+ }
717+ }
718+
463719 Command :: Models { action } => {
464720 let registry = model:: ModelRegistry :: default ( ) ;
465721 match action {
0 commit comments