-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathactivity_cmd.go
More file actions
1677 lines (1464 loc) · 49.6 KB
/
activity_cmd.go
File metadata and controls
1677 lines (1464 loc) · 49.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package main
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/spf13/cobra"
"go.uber.org/zap"
"github.com/smart-mcp-proxy/mcpproxy-go/internal/cli/output"
"github.com/smart-mcp-proxy/mcpproxy-go/internal/cliclient"
"github.com/smart-mcp-proxy/mcpproxy-go/internal/config"
"github.com/smart-mcp-proxy/mcpproxy-go/internal/logs"
"github.com/smart-mcp-proxy/mcpproxy-go/internal/socket"
)
// Activity command flags
var (
// Shared filter flags
activityType string
activityServer string
activityTool string
activityStatus string
activitySessionID string
activityStartTime string
activityEndTime string
activityLimit int
activityOffset int
activityIntentType string // Spec 018: Filter by operation type (read, write, destructive)
activityRequestID string // Spec 021: Filter by HTTP request ID for correlation
activityNoIcons bool // Disable emoji icons in output
activityDetectionType string // Spec 026: Filter by detection type (e.g., "aws_access_key")
activitySeverity string // Spec 026: Filter by severity level (critical, high, medium, low)
activityFlowType string // Spec 027: Filter by flow type (e.g., "internal_to_external")
activityRiskLevel string // Spec 027: Filter by risk level (e.g., "critical", "high")
// Show command flags
activityIncludeResponse bool
// Summary command flags
activityPeriod string
activityGroupBy string
// Export command flags
activityExportOutput string
activityExportFormat string
activityIncludeBodies bool
)
// ActivityFilter contains options for filtering activity records
type ActivityFilter struct {
Type string
Server string
Tool string
Status string
SessionID string
StartTime string
EndTime string
Limit int
Offset int
IntentType string // Spec 018: Filter by operation type (read, write, destructive)
RequestID string // Spec 021: Filter by HTTP request ID for correlation
SensitiveData *bool // Spec 026: Filter by sensitive data detection
DetectionType string // Spec 026: Filter by detection type
Severity string // Spec 026: Filter by severity level
FlowType string // Spec 027: Filter by flow type
RiskLevel string // Spec 027: Filter by risk level
}
// Validate validates the filter options
func (f *ActivityFilter) Validate() error {
// Validate type(s) - supports comma-separated values (Spec 024)
if f.Type != "" {
validTypes := []string{
"tool_call", "policy_decision", "quarantine_change", "server_change",
"system_start", "system_stop", "internal_tool_call", "config_change", // Spec 024: new types
"hook_evaluation", // Spec 027: hook evaluation events
"flow_summary", // Spec 027: flow session summaries
}
// Split by comma for multi-type support
types := strings.Split(f.Type, ",")
for _, t := range types {
t = strings.TrimSpace(t)
valid := false
for _, vt := range validTypes {
if t == vt {
valid = true
break
}
}
if !valid {
return fmt.Errorf("invalid type '%s': must be one of %v", t, validTypes)
}
}
}
// Validate status
if f.Status != "" {
validStatuses := []string{"success", "error", "blocked"}
valid := false
for _, s := range validStatuses {
if f.Status == s {
valid = true
break
}
}
if !valid {
return fmt.Errorf("invalid status '%s': must be one of %v", f.Status, validStatuses)
}
}
// Validate intent_type (Spec 018)
if f.IntentType != "" {
validIntentTypes := []string{"read", "write", "destructive"}
valid := false
for _, it := range validIntentTypes {
if f.IntentType == it {
valid = true
break
}
}
if !valid {
return fmt.Errorf("invalid intent-type '%s': must be one of %v", f.IntentType, validIntentTypes)
}
}
// Validate severity (Spec 026)
if f.Severity != "" {
validSeverities := []string{"critical", "high", "medium", "low"}
valid := false
for _, s := range validSeverities {
if f.Severity == s {
valid = true
break
}
}
if !valid {
return fmt.Errorf("invalid severity '%s': must be one of %v", f.Severity, validSeverities)
}
}
// Validate flow_type (Spec 027)
if f.FlowType != "" {
validFlowTypes := []string{"internal_to_internal", "internal_to_external", "external_to_internal", "external_to_external"}
valid := false
for _, ft := range validFlowTypes {
if f.FlowType == ft {
valid = true
break
}
}
if !valid {
return fmt.Errorf("invalid flow-type '%s': must be one of %v", f.FlowType, validFlowTypes)
}
}
// Validate risk_level (Spec 027)
if f.RiskLevel != "" {
validRiskLevels := []string{"none", "low", "medium", "high", "critical"}
valid := false
for _, rl := range validRiskLevels {
if f.RiskLevel == rl {
valid = true
break
}
}
if !valid {
return fmt.Errorf("invalid risk-level '%s': must be one of %v", f.RiskLevel, validRiskLevels)
}
}
// Validate time formats
if f.StartTime != "" {
if _, err := time.Parse(time.RFC3339, f.StartTime); err != nil {
return fmt.Errorf("invalid start-time format: must be RFC3339 (e.g., 2025-01-01T00:00:00Z)")
}
}
if f.EndTime != "" {
if _, err := time.Parse(time.RFC3339, f.EndTime); err != nil {
return fmt.Errorf("invalid end-time format: must be RFC3339 (e.g., 2025-01-01T00:00:00Z)")
}
}
// Clamp limit
if f.Limit < 1 {
f.Limit = 50
} else if f.Limit > 100 {
f.Limit = 100
}
return nil
}
// ToQueryParams converts filter to URL query parameters
func (f *ActivityFilter) ToQueryParams() url.Values {
q := url.Values{}
if f.Type != "" {
q.Set("type", f.Type)
}
if f.Server != "" {
q.Set("server", f.Server)
}
if f.Tool != "" {
q.Set("tool", f.Tool)
}
if f.Status != "" {
q.Set("status", f.Status)
}
if f.SessionID != "" {
q.Set("session_id", f.SessionID)
}
if f.StartTime != "" {
q.Set("start_time", f.StartTime)
}
if f.EndTime != "" {
q.Set("end_time", f.EndTime)
}
if f.Limit > 0 {
q.Set("limit", fmt.Sprintf("%d", f.Limit))
}
if f.Offset > 0 {
q.Set("offset", fmt.Sprintf("%d", f.Offset))
}
if f.IntentType != "" {
q.Set("intent_type", f.IntentType)
}
// Spec 021: Add request_id filter for log correlation
if f.RequestID != "" {
q.Set("request_id", f.RequestID)
}
// Spec 026: Add sensitive data filters
if f.SensitiveData != nil {
q.Set("sensitive_data", fmt.Sprintf("%t", *f.SensitiveData))
}
if f.DetectionType != "" {
q.Set("detection_type", f.DetectionType)
}
if f.Severity != "" {
q.Set("severity", f.Severity)
}
// Spec 027: Add data flow security filters
if f.FlowType != "" {
q.Set("flow_type", f.FlowType)
}
if f.RiskLevel != "" {
q.Set("risk_level", f.RiskLevel)
}
return q
}
// formatRelativeTime formats a timestamp as relative time for recent events
func formatRelativeTime(t time.Time) string {
now := time.Now()
diff := now.Sub(t)
switch {
case diff < time.Minute:
return "just now"
case diff < time.Hour:
mins := int(diff.Minutes())
if mins == 1 {
return "1 minute ago"
}
return fmt.Sprintf("%d minutes ago", mins)
case diff < 24*time.Hour:
hours := int(diff.Hours())
if hours == 1 {
return "1 hour ago"
}
return fmt.Sprintf("%d hours ago", hours)
case diff < 7*24*time.Hour:
days := int(diff.Hours() / 24)
if days == 1 {
return "1 day ago"
}
return fmt.Sprintf("%d days ago", days)
case t.Year() == now.Year():
return t.Format("Jan 02")
default:
return t.Format("Jan 02, 2006")
}
}
// formatActivityDuration formats duration in milliseconds to human-readable
func formatActivityDuration(ms int64) string {
if ms < 1000 {
return fmt.Sprintf("%dms", ms)
}
return fmt.Sprintf("%.1fs", float64(ms)/1000)
}
// formatSourceIndicator returns an icon/abbreviation for the activity source
func formatSourceIndicator(source string) string {
switch source {
case "mcp":
return "MCP" // AI agent via MCP protocol
case "cli":
return "CLI" // Direct CLI command
case "api":
return "API" // REST API call
default:
return "MCP" // Default to MCP for backwards compatibility
}
}
// formatSourceDescription returns a human-readable description for the activity source
func formatSourceDescription(source string) string {
switch source {
case "mcp":
return "AI agent via MCP protocol"
case "cli":
return "CLI command"
case "api":
return "REST API"
default:
return "AI agent via MCP protocol"
}
}
// formatIntentIndicator extracts intent from activity metadata and returns visual indicator
// Returns emoji indicators: 📖 read, ✏️ write, ⚠️ destructive, or "-" if no intent
func formatIntentIndicator(activity map[string]interface{}) string {
// Extract metadata from activity
metadata := getMapField(activity, "metadata")
if metadata == nil {
return "-"
}
// Extract intent from metadata
intent := getMapField(metadata, "intent")
if intent == nil {
// Check for tool_variant as fallback
if toolVariant := getStringField(metadata, "tool_variant"); toolVariant != "" {
return formatOperationIcon(toolVariantToOperationType(toolVariant))
}
return "-"
}
// Get operation_type from intent
opType := getStringField(intent, "operation_type")
if opType == "" {
return "-"
}
return formatOperationIcon(opType)
}
// formatOperationIcon returns the visual indicator for an operation type
// If activityNoIcons is true, returns text instead of emoji
func formatOperationIcon(opType string) string {
if activityNoIcons {
// Text-only output
switch opType {
case "read":
return "read"
case "write":
return "write"
case "destructive":
return "destructive"
default:
return "-"
}
}
// Emoji output
switch opType {
case "read":
return "📖" // Read operation
case "write":
return "✏️" // Write operation
case "destructive":
return "⚠️" // Destructive operation
default:
return "-"
}
}
// formatSensitiveDataIndicator returns a visual indicator if sensitive data was detected
// Returns "⚠️" (or "SENSITIVE" if no-icons) if detected, "-" otherwise
func formatSensitiveDataIndicator(activity map[string]interface{}) string {
metadata := getMapField(activity, "metadata")
if metadata == nil {
return "-"
}
detection := getMapField(metadata, "sensitive_data_detection")
if detection == nil {
return "-"
}
detected, ok := detection["detected"].(bool)
if !ok || !detected {
return "-"
}
if activityNoIcons {
return "SENSITIVE"
}
return "⚠️"
}
// getSensitiveDataDetection extracts the sensitive data detection result from activity metadata
func getSensitiveDataDetection(activity map[string]interface{}) map[string]interface{} {
metadata := getMapField(activity, "metadata")
if metadata == nil {
return nil
}
return getMapField(metadata, "sensitive_data_detection")
}
// getMaxSeverity returns the highest severity level from detections
func getMaxSeverity(detections []interface{}) string {
severityOrder := map[string]int{
"critical": 4,
"high": 3,
"medium": 2,
"low": 1,
}
maxSeverity := ""
maxOrder := 0
for _, d := range detections {
if detection, ok := d.(map[string]interface{}); ok {
severity := getStringField(detection, "severity")
if order, exists := severityOrder[severity]; exists && order > maxOrder {
maxOrder = order
maxSeverity = severity
}
}
}
return maxSeverity
}
// toolVariantToOperationType converts tool variant name to operation type
func toolVariantToOperationType(variant string) string {
switch variant {
case "call_tool_read":
return "read"
case "call_tool_write":
return "write"
case "call_tool_destructive":
return "destructive"
default:
return ""
}
}
// displayIntentSection displays intent information for activity show command
func displayIntentSection(activity map[string]interface{}) {
// Extract metadata from activity
metadata := getMapField(activity, "metadata")
if metadata == nil {
return
}
// Check if there's any intent-related data
toolVariant := getStringField(metadata, "tool_variant")
intent := getMapField(metadata, "intent")
if toolVariant == "" && intent == nil {
return
}
fmt.Println()
fmt.Println("Intent Declaration:")
// Display tool variant if present
if toolVariant != "" {
opType := toolVariantToOperationType(toolVariant)
fmt.Printf(" Tool Variant: %s\n", toolVariant)
if opType != "" {
fmt.Printf(" Operation Type: %s %s\n", formatOperationIcon(opType), opType)
}
}
// Display intent details if present
if intent != nil {
if opType := getStringField(intent, "operation_type"); opType != "" && toolVariant == "" {
fmt.Printf(" Operation Type: %s %s\n", formatOperationIcon(opType), opType)
}
if sensitivity := getStringField(intent, "data_sensitivity"); sensitivity != "" {
fmt.Printf(" Data Sensitivity: %s\n", sensitivity)
}
if reason := getStringField(intent, "reason"); reason != "" {
fmt.Printf(" Reason: %s\n", reason)
}
if reversible, ok := intent["reversible"].(bool); ok {
reversibleStr := "no"
if reversible {
reversibleStr = "yes"
}
fmt.Printf(" Reversible: %s\n", reversibleStr)
}
}
}
// displaySensitiveDataSection displays sensitive data detection information for activity show command (Spec 026)
func displaySensitiveDataSection(activity map[string]interface{}) {
detection := getSensitiveDataDetection(activity)
if detection == nil {
return
}
detected, ok := detection["detected"].(bool)
if !ok {
return
}
fmt.Println()
fmt.Println("Sensitive Data Detection:")
// Show detection status
if detected {
if activityNoIcons {
fmt.Println(" Status: DETECTED")
} else {
fmt.Println(" Status: \u26a0 DETECTED")
}
} else {
fmt.Println(" Status: No sensitive data detected")
return
}
// Show scan duration if available
if scanMs, ok := detection["scan_duration_ms"].(float64); ok {
fmt.Printf(" Scan Duration: %dms\n", int64(scanMs))
}
// Show if truncated
if truncated, ok := detection["truncated"].(bool); ok && truncated {
fmt.Println(" Note: Payload was truncated for scanning")
}
// Show detections
if detections, ok := detection["detections"].([]interface{}); ok && len(detections) > 0 {
fmt.Println()
fmt.Println(" Detections:")
for i, d := range detections {
if det, ok := d.(map[string]interface{}); ok {
detType := getStringField(det, "type")
category := getStringField(det, "category")
severity := getStringField(det, "severity")
location := getStringField(det, "location")
isExample, _ := det["is_likely_example"].(bool)
fmt.Printf(" [%d] Type: %s\n", i+1, detType)
fmt.Printf(" Category: %s\n", category)
fmt.Printf(" Severity: %s\n", formatSeverityWithColor(severity))
if location != "" {
fmt.Printf(" Location: %s\n", location)
}
if isExample {
fmt.Printf(" Note: Likely an example/test value\n")
}
fmt.Println()
}
}
}
}
// formatSeverityWithColor returns a severity string with visual indicator
func formatSeverityWithColor(severity string) string {
if activityNoIcons {
return severity
}
switch severity {
case "critical":
return "\u2622 " + severity // radioactive symbol for critical
case "high":
return "\u26a0 " + severity // warning sign for high
case "medium":
return "\u26a1 " + severity // lightning for medium
case "low":
return "\u2139 " + severity // info for low
default:
return severity
}
}
// outputActivityError outputs an error in the appropriate format
func outputActivityError(err error, code string) error {
outputFormat := ResolveOutputFormat()
formatter, fmtErr := GetOutputFormatter()
if fmtErr != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return err
}
// T026: Extract request_id from APIError if available
var requestID string
var apiErr *cliclient.APIError
if errors.As(err, &apiErr) && apiErr.HasRequestID() {
requestID = apiErr.RequestID
}
if outputFormat == "json" || outputFormat == "yaml" {
structErr := output.NewStructuredError(code, err.Error()).
WithGuidance("Use 'mcpproxy activity list' to view recent activities").
WithRecoveryCommand("mcpproxy activity list --limit 10")
// T026: Add request_id to StructuredError if available
if requestID != "" {
structErr = structErr.WithRequestID(requestID)
}
result, _ := formatter.FormatError(structErr)
fmt.Println(result)
} else {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
// T026: Include request ID with log retrieval suggestion if available
if requestID != "" {
fmt.Fprintf(os.Stderr, "\nRequest ID: %s\n", requestID)
fmt.Fprintf(os.Stderr, "Use 'mcpproxy activity list --request-id %s' to find related logs.\n", requestID)
}
fmt.Fprintf(os.Stderr, "Hint: Use 'mcpproxy activity list' to view recent activities\n")
}
return err
}
// Activity command definitions
var (
activityCmd = &cobra.Command{
Use: "activity",
Short: "Query and monitor activity logs",
Long: "Commands for listing, watching, and exporting activity logs from the MCPProxy daemon",
}
activityListCmd = &cobra.Command{
Use: "list",
Short: "List activity records with filtering",
Long: `List activity records with optional filtering and pagination.
Examples:
# List recent activity
mcpproxy activity list
# List last 10 tool calls
mcpproxy activity list --type tool_call --limit 10
# List errors from github server
mcpproxy activity list --server github --status error
# List activity by request ID (for error correlation)
mcpproxy activity list --request-id abc123-def456
# List only activities with sensitive data detected
mcpproxy activity list --sensitive-data
# Filter by detection type
mcpproxy activity list --detection-type aws_access_key
# Filter by severity level
mcpproxy activity list --severity critical
# List activity as JSON
mcpproxy activity list -o json`,
RunE: runActivityList,
}
activityWatchCmd = &cobra.Command{
Use: "watch",
Short: "Watch activity stream in real-time",
Long: `Watch activity events in real-time via SSE stream.
Examples:
# Watch all activity
mcpproxy activity watch
# Watch only tool calls from github
mcpproxy activity watch --type tool_call --server github
# Watch with JSON output
mcpproxy activity watch -o json`,
RunE: runActivityWatch,
}
activityShowCmd = &cobra.Command{
Use: "show <id>",
Short: "Show activity details",
Long: `Show full details of a specific activity record.
Examples:
# Show activity details
mcpproxy activity show 01JFXYZ123ABC
# Show with full response body
mcpproxy activity show 01JFXYZ123ABC --include-response`,
Args: cobra.ExactArgs(1),
RunE: runActivityShow,
}
activitySummaryCmd = &cobra.Command{
Use: "summary",
Short: "Show activity statistics",
Long: `Show aggregated activity statistics for a time period.
Examples:
# Show 24-hour summary
mcpproxy activity summary
# Show weekly summary
mcpproxy activity summary --period 7d
# Show summary grouped by server
mcpproxy activity summary --by server`,
RunE: runActivitySummary,
}
activityExportCmd = &cobra.Command{
Use: "export",
Short: "Export activity records",
Long: `Export activity records for compliance and auditing.
Examples:
# Export all activity as JSON Lines to file
mcpproxy activity export --output activity.jsonl
# Export as CSV
mcpproxy activity export --format csv --output activity.csv
# Export to stdout for piping
mcpproxy activity export --format csv | gzip > activity.csv.gz`,
RunE: runActivityExport,
}
)
// GetActivityCommand returns the activity command for registration
func GetActivityCommand() *cobra.Command {
return activityCmd
}
func init() {
// Add subcommands
activityCmd.AddCommand(activityListCmd)
activityCmd.AddCommand(activityWatchCmd)
activityCmd.AddCommand(activityShowCmd)
activityCmd.AddCommand(activitySummaryCmd)
activityCmd.AddCommand(activityExportCmd)
// List command flags
activityListCmd.Flags().StringVarP(&activityType, "type", "t", "", "Filter by type (comma-separated for multiple): tool_call, system_start, system_stop, internal_tool_call, config_change, policy_decision, quarantine_change, server_change, hook_evaluation, flow_summary")
activityListCmd.Flags().StringVarP(&activityServer, "server", "s", "", "Filter by server name")
activityListCmd.Flags().StringVar(&activityTool, "tool", "", "Filter by tool name")
activityListCmd.Flags().StringVar(&activityStatus, "status", "", "Filter by status: success, error, blocked")
activityListCmd.Flags().StringVar(&activitySessionID, "session", "", "Filter by MCP session ID")
activityListCmd.Flags().StringVar(&activityStartTime, "start-time", "", "Filter records after this time (RFC3339)")
activityListCmd.Flags().StringVar(&activityEndTime, "end-time", "", "Filter records before this time (RFC3339)")
activityListCmd.Flags().IntVarP(&activityLimit, "limit", "n", 50, "Max records to return (1-100)")
activityListCmd.Flags().IntVar(&activityOffset, "offset", 0, "Pagination offset")
activityListCmd.Flags().StringVar(&activityIntentType, "intent-type", "", "Filter by intent operation type: read, write, destructive")
activityListCmd.Flags().StringVar(&activityRequestID, "request-id", "", "Filter by HTTP request ID for log correlation")
activityListCmd.Flags().BoolVar(&activityNoIcons, "no-icons", false, "Disable emoji icons in output (use text instead)")
// Spec 026: Sensitive data detection filters
activityListCmd.Flags().Bool("sensitive-data", false, "Filter to show only activities with sensitive data detected")
activityListCmd.Flags().StringVar(&activityDetectionType, "detection-type", "", "Filter by detection type (e.g., aws_access_key, stripe_key)")
activityListCmd.Flags().StringVar(&activitySeverity, "severity", "", "Filter by severity level: critical, high, medium, low")
// Spec 027: Data flow security filters
activityListCmd.Flags().StringVar(&activityFlowType, "flow-type", "", "Filter by data flow type: internal_to_internal, internal_to_external, external_to_internal, external_to_external")
activityListCmd.Flags().StringVar(&activityRiskLevel, "risk-level", "", "Filter by risk level (>= comparison): none, low, medium, high, critical")
// Watch command flags
activityWatchCmd.Flags().StringVarP(&activityType, "type", "t", "", "Filter by type (comma-separated): tool_call, system_start, system_stop, internal_tool_call, config_change, policy_decision, quarantine_change, server_change")
activityWatchCmd.Flags().StringVarP(&activityServer, "server", "s", "", "Filter by server name")
// Show command flags
activityShowCmd.Flags().BoolVar(&activityIncludeResponse, "include-response", false, "Show full response (may be large)")
activityShowCmd.Flags().BoolVar(&activityNoIcons, "no-icons", false, "Disable emoji icons in output (use text instead)")
// Summary command flags
activitySummaryCmd.Flags().StringVarP(&activityPeriod, "period", "p", "24h", "Time period: 1h, 24h, 7d, 30d")
activitySummaryCmd.Flags().StringVar(&activityGroupBy, "by", "", "Group by: server, tool, status")
// Export command flags
activityExportCmd.Flags().StringVar(&activityExportOutput, "output", "", "Output file path (stdout if not specified)")
activityExportCmd.Flags().StringVarP(&activityExportFormat, "format", "f", "json", "Export format: json, csv")
activityExportCmd.Flags().BoolVar(&activityIncludeBodies, "include-bodies", false, "Include full request/response bodies")
// Reuse list filter flags for export
activityExportCmd.Flags().StringVarP(&activityType, "type", "t", "", "Filter by type (comma-separated): tool_call, system_start, system_stop, internal_tool_call, config_change, policy_decision, quarantine_change, server_change")
activityExportCmd.Flags().StringVarP(&activityServer, "server", "s", "", "Filter by server name")
activityExportCmd.Flags().StringVar(&activityTool, "tool", "", "Filter by tool name")
activityExportCmd.Flags().StringVar(&activityStatus, "status", "", "Filter by status")
activityExportCmd.Flags().StringVar(&activitySessionID, "session", "", "Filter by session ID")
activityExportCmd.Flags().StringVar(&activityStartTime, "start-time", "", "Filter after this time (RFC3339)")
activityExportCmd.Flags().StringVar(&activityEndTime, "end-time", "", "Filter before this time (RFC3339)")
}
// getActivityClient creates an HTTP client for the daemon
func getActivityClient(logger *zap.SugaredLogger) (*cliclient.Client, error) {
// Load config - use explicit config file if provided via -c flag
var cfg *config.Config
var err error
if configFile != "" {
cfg, err = config.LoadFromFile(configFile)
} else {
cfg, err = config.Load()
}
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
}
// Try socket first, then HTTP
endpoint := socket.GetDefaultSocketPath(cfg.DataDir)
if cfg.Listen != "" {
// Check if socket exists (use IsSocketAvailable which handles unix:// prefix)
if !socket.IsSocketAvailable(endpoint) {
// Handle listen addresses like ":8080" (no host)
listen := cfg.Listen
if strings.HasPrefix(listen, ":") {
listen = "127.0.0.1" + listen
}
endpoint = "http://" + listen
}
}
return cliclient.NewClientWithAPIKey(endpoint, cfg.APIKey, logger), nil
}
// runActivityList implements the activity list command
func runActivityList(cmd *cobra.Command, _ []string) error {
// Setup logger
cmdLogLevel, _ := cmd.Flags().GetString("log-level")
cmdLogToFile, _ := cmd.Flags().GetBool("log-to-file")
cmdLogDir, _ := cmd.Flags().GetString("log-dir")
logger, err := logs.SetupCommandLogger(false, cmdLogLevel, cmdLogToFile, cmdLogDir)
if err != nil {
return fmt.Errorf("failed to setup logger: %w", err)
}
defer func() { _ = logger.Sync() }()
// Spec 026: Handle sensitive-data flag
var sensitiveDataPtr *bool
if cmd.Flags().Changed("sensitive-data") {
sensitiveDataVal, _ := cmd.Flags().GetBool("sensitive-data")
sensitiveDataPtr = &sensitiveDataVal
}
// Build filter
filter := &ActivityFilter{
Type: activityType,
Server: activityServer,
Tool: activityTool,
Status: activityStatus,
SessionID: activitySessionID,
StartTime: activityStartTime,
EndTime: activityEndTime,
Limit: activityLimit,
Offset: activityOffset,
IntentType: activityIntentType,
RequestID: activityRequestID,
SensitiveData: sensitiveDataPtr,
DetectionType: activityDetectionType,
Severity: activitySeverity,
FlowType: activityFlowType,
RiskLevel: activityRiskLevel,
}
if err := filter.Validate(); err != nil {
return outputActivityError(err, "INVALID_FILTER")
}
// Create client
client, err := getActivityClient(logger.Sugar())
if err != nil {
return outputActivityError(err, "CONNECTION_ERROR")
}
// Fetch activities
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
activities, total, err := client.ListActivities(ctx, filter)
if err != nil {
return outputActivityError(err, "FETCH_ERROR")
}
// Format output
outputFormat := ResolveOutputFormat()
formatter, err := GetOutputFormatter()
if err != nil {
return err
}
if outputFormat == "json" || outputFormat == "yaml" {
data := map[string]interface{}{
"activities": activities,
"total": total,
"limit": filter.Limit,
"offset": filter.Offset,
}
result, err := formatter.Format(data)
if err != nil {
return err
}
fmt.Println(result)
return nil
}
// Table output
if len(activities) == 0 {
fmt.Println("No activities found")
return nil
}
// Spec 026: Add SENSITIVE column to indicate activities with sensitive data detected
headers := []string{"ID", "SRC", "TYPE", "SERVER", "TOOL", "INTENT", "SENSITIVE", "STATUS", "DURATION", "TIME"}
rows := make([][]string, 0, len(activities))
for _, act := range activities {
id := getStringField(act, "id")
source := getStringField(act, "source")
actType := getStringField(act, "type")
server := getStringField(act, "server_name")
tool := getStringField(act, "tool_name")
status := getStringField(act, "status")
durationMs := getIntField(act, "duration_ms")
timestamp := getStringField(act, "timestamp")
// Extract intent from metadata (Spec 018)
intentStr := formatIntentIndicator(act)
// Spec 026: Format sensitive data indicator
sensitiveStr := formatSensitiveDataIndicator(act)
// Parse and format timestamp
timeStr := timestamp
if t, err := time.Parse(time.RFC3339, timestamp); err == nil {
timeStr = formatRelativeTime(t)
}
// Truncate type for display
if len(actType) > 12 {
actType = actType[:12]
}
// Format source indicator
sourceIcon := formatSourceIndicator(source)
rows = append(rows, []string{
id, // Show full ID so it can be used with 'activity show'
sourceIcon,
actType,
server,
tool,
intentStr,
sensitiveStr, // Spec 026: Show sensitive data indicator
status,
formatActivityDuration(int64(durationMs)),
timeStr,
})
}
result, err := formatter.FormatTable(headers, rows)
if err != nil {
return err
}
fmt.Print(result)
// Show pagination info
fmt.Printf("\nShowing %d of %d records", len(activities), total)
if filter.Offset > 0 || total > filter.Limit {
page := (filter.Offset / filter.Limit) + 1
fmt.Printf(" (page %d)", page)
}
fmt.Println()
return nil
}
// runActivityWatch implements the activity watch command
func runActivityWatch(cmd *cobra.Command, _ []string) error {
// Setup logger
cmdLogLevel, _ := cmd.Flags().GetString("log-level")
cmdLogToFile, _ := cmd.Flags().GetBool("log-to-file")
cmdLogDir, _ := cmd.Flags().GetString("log-dir")
logger, err := logs.SetupCommandLogger(false, cmdLogLevel, cmdLogToFile, cmdLogDir)
if err != nil {
return fmt.Errorf("failed to setup logger: %w", err)
}
defer func() { _ = logger.Sync() }()
// Load config to get endpoint - use same logic as getActivityClient
var cfg *config.Config
if configFile != "" {
cfg, err = config.LoadFromFile(configFile)
} else {
cfg, err = config.Load()
}