Skip to content

Commit 5feb92b

Browse files
authored
feat: Add support for cat command (#17)
1 parent 44b5ac7 commit 5feb92b

File tree

8 files changed

+362
-0
lines changed

8 files changed

+362
-0
lines changed

cli/cat.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/google/uuid"
9+
"github.com/safedep/dry/log"
10+
"github.com/safedep/gryph/core/events"
11+
"github.com/safedep/gryph/tui"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
func NewCatCmd() *cobra.Command {
16+
var format string
17+
18+
cmd := &cobra.Command{
19+
Use: "cat <event-id> [event-id...]",
20+
Short: "Show full details of one or more events",
21+
Long: `Show full details of one or more events.
22+
23+
Displays all fields including payload, diff content, raw event,
24+
and conversation context. Accepts full UUIDs or ID prefixes.`,
25+
Example: ` gryph cat a1b2c3d4
26+
gryph cat a1b2c3d4-e5f6-7890-abcd-ef1234567890
27+
gryph cat a1b2c3d4 f5e6d7c8 --format json
28+
gryph cat a1b2c3d4 --format jsonl`,
29+
Args: cobra.MinimumNArgs(1),
30+
RunE: func(cmd *cobra.Command, args []string) error {
31+
ctx := context.Background()
32+
33+
app, err := loadApp()
34+
if err != nil {
35+
return err
36+
}
37+
38+
app.Presenter = tui.NewPresenter(getFormat(format), tui.PresenterOptions{
39+
Writer: cmd.OutOrStdout(),
40+
UseColors: app.Config.ShouldUseColors(),
41+
})
42+
43+
if err := app.InitStore(ctx); err != nil {
44+
return ErrDatabase("failed to open database", err)
45+
}
46+
defer func() {
47+
err := app.Close()
48+
if err != nil {
49+
log.Errorf("failed to close app: %w", err)
50+
}
51+
}()
52+
53+
views := make([]*tui.EventDetailView, 0, len(args))
54+
for _, idArg := range args {
55+
event, err := resolveEvent(ctx, app, idArg)
56+
if err != nil {
57+
return err
58+
}
59+
views = append(views, eventToDetailView(event))
60+
}
61+
62+
return app.Presenter.RenderEventDetails(views)
63+
},
64+
}
65+
66+
cmd.Flags().StringVar(&format, "format", "table", "output format: table, json, jsonl, csv")
67+
68+
return cmd
69+
}
70+
71+
func resolveEvent(ctx context.Context, app *App, idArg string) (*events.Event, error) {
72+
eventID, err := uuid.Parse(idArg)
73+
if err != nil {
74+
e, err := app.Store.GetEventByPrefix(ctx, idArg)
75+
if err != nil {
76+
return nil, fmt.Errorf("event not found: %s", idArg)
77+
}
78+
if e == nil {
79+
return nil, fmt.Errorf("event not found: %s", idArg)
80+
}
81+
return e, nil
82+
}
83+
84+
event, err := app.Store.GetEvent(ctx, eventID)
85+
if err != nil {
86+
return nil, fmt.Errorf("failed to get event: %w", err)
87+
}
88+
if event == nil {
89+
return nil, fmt.Errorf("event not found: %s", idArg)
90+
}
91+
return event, nil
92+
}
93+
94+
func eventToDetailView(e *events.Event) *tui.EventDetailView {
95+
view := &tui.EventDetailView{
96+
ID: e.ID.String(),
97+
SessionID: e.SessionID.String(),
98+
AgentSessionID: e.AgentSessionID,
99+
Sequence: e.Sequence,
100+
Timestamp: e.Timestamp,
101+
DurationMs: e.DurationMs,
102+
AgentName: e.AgentName,
103+
AgentDisplayName: getAgentDisplayName(e.AgentName),
104+
AgentVersion: e.AgentVersion,
105+
WorkingDirectory: e.WorkingDirectory,
106+
ActionType: string(e.ActionType),
107+
ActionDisplay: e.ActionType.DisplayName(),
108+
ToolName: e.ToolName,
109+
ResultStatus: string(e.ResultStatus),
110+
ErrorMessage: e.ErrorMessage,
111+
IsSensitive: e.IsSensitive,
112+
DiffContent: e.DiffContent,
113+
RawEvent: e.RawEvent,
114+
ConvContext: e.ConversationContext,
115+
}
116+
117+
switch e.ActionType {
118+
case events.ActionFileRead:
119+
if p, err := e.GetFileReadPayload(); err == nil && p != nil {
120+
view.Payload = p
121+
}
122+
case events.ActionFileWrite:
123+
if p, err := e.GetFileWritePayload(); err == nil && p != nil {
124+
view.Payload = p
125+
}
126+
case events.ActionCommandExec:
127+
if p, err := e.GetCommandExecPayload(); err == nil && p != nil {
128+
view.Payload = p
129+
}
130+
case events.ActionToolUse:
131+
if p, err := e.GetToolUsePayload(); err == nil && p != nil {
132+
view.Payload = p
133+
}
134+
default:
135+
if len(e.Payload) > 0 {
136+
var raw any
137+
if json.Unmarshal(e.Payload, &raw) == nil {
138+
view.Payload = raw
139+
}
140+
}
141+
}
142+
143+
return view
144+
}

cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ native hook systems to create a comprehensive audit trail of all agent actions.`
158158
NewConfigCmd(),
159159
NewSelfLogCmd(),
160160
NewDiffCmd(),
161+
NewCatCmd(),
161162
NewHookCmd(),
162163
NewRetentionCmd(),
163164
NewStreamCmd(),

tui/csv.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package tui
22

33
import (
44
"encoding/csv"
5+
"encoding/json"
56
"fmt"
67
"io"
78
)
@@ -293,5 +294,58 @@ func (p *CSVPresenter) RenderUpdateNotice(notice *UpdateNoticeView) error {
293294
return p.writer.Error()
294295
}
295296

297+
// RenderEventDetails renders full details of one or more events as CSV.
298+
func (p *CSVPresenter) RenderEventDetails(events []*EventDetailView) error {
299+
_ = p.writer.Write([]string{
300+
"id", "session_id", "agent_session_id", "sequence", "timestamp",
301+
"duration_ms", "agent_name", "agent_version", "working_directory",
302+
"action_type", "tool_name", "result_status", "error_message",
303+
"sensitive", "payload", "diff_content", "raw_event", "conversation_context",
304+
})
305+
306+
for _, e := range events {
307+
sensitive := "false"
308+
if e.IsSensitive {
309+
sensitive = "true"
310+
}
311+
312+
payloadStr := ""
313+
if e.Payload != nil {
314+
if data, err := json.Marshal(e.Payload); err == nil {
315+
payloadStr = string(data)
316+
}
317+
}
318+
319+
rawEventStr := ""
320+
if len(e.RawEvent) > 0 {
321+
rawEventStr = string(e.RawEvent)
322+
}
323+
324+
_ = p.writer.Write([]string{
325+
e.ID,
326+
e.SessionID,
327+
e.AgentSessionID,
328+
fmt.Sprintf("%d", e.Sequence),
329+
FormatTime(e.Timestamp),
330+
fmt.Sprintf("%d", e.DurationMs),
331+
e.AgentName,
332+
e.AgentVersion,
333+
e.WorkingDirectory,
334+
e.ActionType,
335+
e.ToolName,
336+
e.ResultStatus,
337+
e.ErrorMessage,
338+
sensitive,
339+
payloadStr,
340+
e.DiffContent,
341+
rawEventStr,
342+
e.ConvContext,
343+
})
344+
}
345+
346+
p.writer.Flush()
347+
return p.writer.Error()
348+
}
349+
296350
// Ensure CSVPresenter implements Presenter
297351
var _ Presenter = (*CSVPresenter)(nil)

tui/json.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,5 +108,10 @@ func (p *JSONPresenter) RenderUpdateNotice(notice *UpdateNoticeView) error {
108108
return p.encoder.Encode(notice)
109109
}
110110

111+
// RenderEventDetails renders full details of one or more events as JSON.
112+
func (p *JSONPresenter) RenderEventDetails(events []*EventDetailView) error {
113+
return p.encoder.Encode(events)
114+
}
115+
111116
// Ensure JSONPresenter implements Presenter
112117
var _ Presenter = (*JSONPresenter)(nil)

tui/jsonl.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,5 +136,15 @@ func (p *JSONLPresenter) RenderUpdateNotice(notice *UpdateNoticeView) error {
136136
return p.encoder.Encode(notice)
137137
}
138138

139+
// RenderEventDetails renders full details of one or more events as JSONL.
140+
func (p *JSONLPresenter) RenderEventDetails(events []*EventDetailView) error {
141+
for _, e := range events {
142+
if err := p.encoder.Encode(e); err != nil {
143+
return err
144+
}
145+
}
146+
return nil
147+
}
148+
139149
// Ensure JSONLPresenter implements Presenter
140150
var _ Presenter = (*JSONLPresenter)(nil)

tui/table.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package tui
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"io"
67
"strings"
8+
"time"
79
)
810

911
// TablePresenter renders output in table format.
@@ -583,5 +585,123 @@ func (p *TablePresenter) RenderUpdateNotice(notice *UpdateNoticeView) error {
583585
return tw.Err()
584586
}
585587

588+
// RenderEventDetails renders full details of one or more events.
589+
func (p *TablePresenter) RenderEventDetails(events []*EventDetailView) error {
590+
tw := &tableWriter{w: p.w}
591+
592+
for i, e := range events {
593+
if i > 0 {
594+
tw.println()
595+
tw.println("---")
596+
tw.println()
597+
}
598+
599+
tw.printf("%-16s %s\n", "Event", e.ID)
600+
tw.printf("%-16s %s\n", "Session", e.SessionID)
601+
if e.AgentSessionID != "" {
602+
tw.printf("%-16s %s\n", "Agent Session", p.color.Dim(e.AgentSessionID))
603+
}
604+
tw.printf("%-16s %d\n", "Sequence", e.Sequence)
605+
tw.printf("%-16s %s\n", "Timestamp", FormatTime(e.Timestamp))
606+
if e.DurationMs > 0 {
607+
tw.printf("%-16s %s\n", "Duration", FormatDuration(time.Duration(e.DurationMs)*time.Millisecond))
608+
}
609+
tw.println()
610+
611+
agentStr := p.color.Agent(e.AgentDisplayName)
612+
if e.AgentVersion != "" {
613+
agentStr += " " + e.AgentVersion
614+
}
615+
tw.printf("%-16s %s\n", "Agent", agentStr)
616+
if e.WorkingDirectory != "" {
617+
tw.printf("%-16s %s\n", "Directory", p.color.Path(e.WorkingDirectory))
618+
}
619+
tw.println()
620+
621+
tw.printf("%-16s %s (%s)\n", "Action", e.ActionDisplay, e.ActionType)
622+
if e.ToolName != "" {
623+
tw.printf("%-16s %s\n", "Tool", e.ToolName)
624+
}
625+
tw.printf("%-16s %s\n", "Status", p.formatResultStatus(e.ResultStatus))
626+
if e.ErrorMessage != "" {
627+
tw.printf("%-16s %s\n", "Error", p.color.Error(e.ErrorMessage))
628+
}
629+
if e.IsSensitive {
630+
tw.printf("%-16s %s\n", "Sensitive", p.color.Warning("yes"))
631+
}
632+
633+
if e.Payload != nil {
634+
tw.println()
635+
tw.printf("%s\n", p.color.Header("Payload"))
636+
p.renderPayloadDetail(tw, e.Payload)
637+
}
638+
639+
if e.DiffContent != "" {
640+
tw.println()
641+
tw.printf("%s\n", p.color.Header("Diff"))
642+
for _, line := range strings.Split(e.DiffContent, "\n") {
643+
if strings.HasPrefix(line, "+++") || strings.HasPrefix(line, "---") {
644+
tw.println(p.color.DiffHeader(line))
645+
} else if strings.HasPrefix(line, "+") {
646+
tw.println(p.color.DiffAdd(line))
647+
} else if strings.HasPrefix(line, "-") {
648+
tw.println(p.color.DiffRemove(line))
649+
} else if strings.HasPrefix(line, "@@") {
650+
tw.println(p.color.Cyan(line))
651+
} else {
652+
tw.println(line)
653+
}
654+
}
655+
}
656+
657+
if len(e.RawEvent) > 0 {
658+
tw.println()
659+
tw.printf("%s\n", p.color.Header("Raw Event"))
660+
var pretty json.RawMessage
661+
if json.Unmarshal(e.RawEvent, &pretty) == nil {
662+
formatted, err := json.MarshalIndent(pretty, " ", " ")
663+
if err == nil {
664+
tw.printf(" %s\n", string(formatted))
665+
}
666+
}
667+
}
668+
669+
if e.ConvContext != "" {
670+
tw.println()
671+
tw.printf("%s\n", p.color.Header("Conversation Context"))
672+
tw.printf(" %s\n", e.ConvContext)
673+
}
674+
}
675+
676+
return tw.Err()
677+
}
678+
679+
func (p *TablePresenter) formatResultStatus(status string) string {
680+
switch status {
681+
case "success":
682+
return p.color.Success(status)
683+
case "error":
684+
return p.color.Error(status)
685+
case "blocked", "rejected":
686+
return p.color.Warning(status)
687+
default:
688+
return status
689+
}
690+
}
691+
692+
func (p *TablePresenter) renderPayloadDetail(tw *tableWriter, payload any) {
693+
switch v := payload.(type) {
694+
case map[string]any:
695+
for key, val := range v {
696+
tw.printf(" %-16s %v\n", key, val)
697+
}
698+
default:
699+
data, err := json.MarshalIndent(v, " ", " ")
700+
if err == nil {
701+
tw.printf(" %s\n", string(data))
702+
}
703+
}
704+
}
705+
586706
// Ensure TablePresenter implements Presenter
587707
var _ Presenter = (*TablePresenter)(nil)

tui/tui.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ type Presenter interface {
3434
// RenderEvents renders a list of events.
3535
RenderEvents(events []*EventView) error
3636

37+
// RenderEventDetails renders full details of one or more events.
38+
RenderEventDetails(events []*EventDetailView) error
39+
3740
// RenderInstall renders the installation result.
3841
RenderInstall(result *InstallView) error
3942

0 commit comments

Comments
 (0)