@@ -165,9 +165,29 @@ func describeCommands(command string) (cliSchema, error) {
165165 "fincrawl search \" billing refund\" --limit 10" ,
166166 "fincrawl search \" billing refund\" --state open --tag billing" ,
167167 "fincrawl search \" login\" --fin-status resolved" ,
168- "fincrawl search \" billing refund\" --fields provider_id,subject,updated_at --ndjson" ,
168+ "fincrawl search \" billing refund\" --fields provider_id,subject,score, updated_at --ndjson" ,
169169 },
170- Notes : []string {"Allowed fields: id, provider_id, subject, state, assignee, rating, fin_status, participants, tags, updated_at, snippet." },
170+ Notes : []string {"Allowed fields: id, provider_id, subject, state, assignee, rating, fin_status, participants, tags, updated_at, snippet, score." },
171+ },
172+ "show" : {
173+ Name : "show" ,
174+ Summary : "Show one local conversation by local ID or provider ID." ,
175+ JSON : true ,
176+ Args : []paramSchema {
177+ {Name : "id" , Type : "conversation-id|provider-id" , Required : true , Help : "Conversation local ID or provider ID." },
178+ },
179+ Flags : []paramSchema {
180+ {Name : "fields" , Type : "field-list" , Help : "Comma-separated fields to include in output." },
181+ {Name : "parts" , Type : "bool" , Help : "Include sanitized conversation parts." },
182+ {Name : "part-limit" , Type : "int" , Default : "20" , Help : "Maximum parts when --parts is set." },
183+ {Name : "json" , Type : "bool" , Default : "true" , Help : "Print JSON output." },
184+ },
185+ Examples : []string {
186+ "fincrawl show <provider-conversation-id>" ,
187+ "fincrawl show <provider-conversation-id> --fields provider_id,subject,tags,snippet" ,
188+ "fincrawl show <provider-conversation-id> --parts --part-limit 5" ,
189+ },
190+ Notes : []string {"Allowed fields: id, provider_id, subject, state, assignee, rating, fin_status, participants, tags, created_at, updated_at, snippet, parts." , "Parts are opt-in and sanitized before output." },
171191 },
172192 "archive" : {
173193 Name : "archive" ,
@@ -218,6 +238,22 @@ func describeCommands(command string) (cliSchema, error) {
218238 "Input paths must be relative, stay under the current working directory, and end in .jsonl.zst.age." ,
219239 },
220240 },
241+ "store verify" : {
242+ Name : "store verify" ,
243+ Summary : "Verify a generic encrypted tenant-store manifest and artifact boundary." ,
244+ JSON : true ,
245+ Args : []paramSchema {
246+ {Name : "path" , Type : "path" , Default : "." , Help : "Tenant store root containing manifest.json." },
247+ },
248+ Flags : []paramSchema {
249+ {Name : "json" , Type : "bool" , Default : "true" , Help : "Print JSON output." },
250+ },
251+ Examples : []string {"fincrawl store verify ." , "fincrawl store verify ../tenant-store --json" },
252+ Notes : []string {
253+ "Manifest snapshots must reference existing .jsonl.zst.age or .tar.zst.age files with relative paths." ,
254+ "Plaintext archives, local databases, runtime state, logs, reports, screenshots, and transcripts are rejected." ,
255+ },
256+ },
221257 "guard" : {
222258 Name : "guard" ,
223259 Summary : "Check commit guardrails for tenant data, plaintext archives, and secret-like values." ,
@@ -241,6 +277,9 @@ func describeCommands(command string) (cliSchema, error) {
241277 if command == "" {
242278 return schema , nil
243279 }
280+ if command == "store" {
281+ command = "store verify"
282+ }
244283 described , ok := schema .Commands [command ]
245284 if ! ok {
246285 return cliSchema {}, output.UsageError {Err : fmt .Errorf ("unknown command %q" , command )}
@@ -547,11 +586,88 @@ func searchResultField(result store.SearchResult, name string) (any, error) {
547586 return result .UpdatedAt , nil
548587 case "snippet" :
549588 return result .Snippet , nil
589+ case "score" :
590+ return result .Score , nil
550591 default :
551592 return nil , fmt .Errorf ("unknown search field %q" , name )
552593 }
553594}
554595
596+ func projectConversationDetail (detail store.ConversationDetail , fields string ) (any , error ) {
597+ fields = strings .TrimSpace (fields )
598+ if fields == "" {
599+ return detail , nil
600+ }
601+ names , err := parseConversationFields (fields )
602+ if err != nil {
603+ return nil , err
604+ }
605+ projected := make (map [string ]any , len (names ))
606+ for _ , name := range names {
607+ value , _ := conversationDetailField (detail , name )
608+ projected [name ] = value
609+ }
610+ return projected , nil
611+ }
612+
613+ func validateConversationFields (fields string ) error {
614+ fields = strings .TrimSpace (fields )
615+ if fields == "" {
616+ return nil
617+ }
618+ _ , err := parseConversationFields (fields )
619+ return err
620+ }
621+
622+ func parseConversationFields (fields string ) ([]string , error ) {
623+ rawNames := strings .Split (fields , "," )
624+ names := make ([]string , 0 , len (rawNames ))
625+ for _ , name := range rawNames {
626+ name = strings .TrimSpace (name )
627+ if name == "" {
628+ return nil , errors .New ("--fields contains an empty field" )
629+ }
630+ if _ , err := conversationDetailField (store.ConversationDetail {}, name ); err != nil {
631+ return nil , err
632+ }
633+ names = append (names , name )
634+ }
635+ return names , nil
636+ }
637+
638+ func conversationDetailField (detail store.ConversationDetail , name string ) (any , error ) {
639+ switch name {
640+ case "id" :
641+ return detail .ID , nil
642+ case "provider_id" :
643+ return detail .ProviderID , nil
644+ case "subject" :
645+ return detail .Subject , nil
646+ case "state" :
647+ return detail .State , nil
648+ case "assignee" :
649+ return detail .Assignee , nil
650+ case "rating" :
651+ return detail .Rating , nil
652+ case "fin_status" :
653+ return detail .FinStatus , nil
654+ case "participants" :
655+ return detail .Participants , nil
656+ case "tags" :
657+ return detail .Tags , nil
658+ case "created_at" :
659+ return detail .CreatedAt , nil
660+ case "updated_at" :
661+ return detail .UpdatedAt , nil
662+ case "snippet" :
663+ return detail .Snippet , nil
664+ case "parts" :
665+ return detail .Parts , nil
666+ default :
667+ return nil , fmt .Errorf ("unknown show field %q" , name )
668+ }
669+ }
670+
555671func hasControlOrSpace (value string ) bool {
556672 for _ , r := range value {
557673 if unicode .IsControl (r ) || unicode .IsSpace (r ) {
0 commit comments