Skip to content

Commit 8c0898c

Browse files
authored
feat(session): add session list + export commands for AI SRE (#30)
session list (--app default ai-sre, --limit, --scope, --status, --team-id, --since client-side filter, --format jsonl/json/toon) paginates server-side to honor --limit beyond the handler's lte=100 page cap. session export <id> streams the NDJSON session transcript to stdout. Pins go-flashduty to the merged main commit (go-flashduty#8). Backs the /insight skill's collect.sh.
1 parent e17ab0e commit 8c0898c

11 files changed

Lines changed: 1048 additions & 15 deletions

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/flashcatcloud/flashduty-cli
33
go 1.25.1
44

55
require (
6-
github.com/flashcatcloud/go-flashduty v0.5.3-0.20260602040240-b12fb6a1ddb2
6+
github.com/flashcatcloud/go-flashduty v0.5.4-0.20260602051355-7583ebae5b07
77
github.com/mattn/go-runewidth v0.0.23
88
github.com/spf13/cobra v1.10.2
99
github.com/spf13/pflag v1.0.10

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
22
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
33
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
4-
github.com/flashcatcloud/go-flashduty v0.5.3-0.20260602040240-b12fb6a1ddb2 h1:Tr563N4JAbclxnC9dWmwyPC39SCc/bifW0eVvCcnSyk=
5-
github.com/flashcatcloud/go-flashduty v0.5.3-0.20260602040240-b12fb6a1ddb2/go.mod h1:aA0RtZEs0AYOwwdNKdtVeD8YMOdnmVY1zAlVD+9Ovx8=
4+
github.com/flashcatcloud/go-flashduty v0.5.4-0.20260602051355-7583ebae5b07 h1:bi1rOjR2OY+TovBGabtVOTcEQWlgzU9RfEwlJxU+3n8=
5+
github.com/flashcatcloud/go-flashduty v0.5.4-0.20260602051355-7583ebae5b07/go.mod h1:aA0RtZEs0AYOwwdNKdtVeD8YMOdnmVY1zAlVD+9Ovx8=
66
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
77
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
88
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=

internal/cli/coverage_test.go

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,20 @@ import (
1111
"github.com/spf13/cobra"
1212
)
1313

14-
// loadSpecPaths reads every GET/POST operation from the openapi spec shipped in
15-
// the linked go-flashduty module — the same spec cligen generates against —
16-
// returning operationId -> path.
17-
func loadSpecPaths(t *testing.T) map[string]string {
14+
// specOpMeta is the slice of an operation the coverage tests reason about.
15+
type specOpMeta struct {
16+
id string
17+
path string
18+
streaming bool // 200 body is not application/json (e.g. application/x-ndjson)
19+
}
20+
21+
// loadSpecOps reads every public GET/POST operation from the openapi spec
22+
// shipped in the linked go-flashduty module — the same spec cligen generates
23+
// against — recording each op's id, path, and whether its 200 response is a
24+
// non-JSON streaming body. Streaming ops are served by curated commands (the
25+
// generated typed-response template cannot model an io.ReadCloser), so the
26+
// generator-coverage check excludes them.
27+
func loadSpecOps(t *testing.T) []specOpMeta {
1828
t.Helper()
1929
out, err := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", "github.com/flashcatcloud/go-flashduty").Output()
2030
if err != nil {
@@ -29,12 +39,15 @@ func loadSpecPaths(t *testing.T) map[string]string {
2939
Paths map[string]map[string]struct {
3040
OperationID string `json:"operationId"`
3141
Tags []string `json:"tags"`
42+
Responses map[string]struct {
43+
Content map[string]json.RawMessage `json:"content"`
44+
} `json:"responses"`
3245
} `json:"paths"`
3346
}
3447
if err := json.Unmarshal(data, &spec); err != nil {
3548
t.Fatalf("parse spec: %v", err)
3649
}
37-
ids := map[string]string{}
50+
var ops []specOpMeta
3851
for path, methods := range spec.Paths {
3952
for verb, op := range methods {
4053
v := strings.ToUpper(verb)
@@ -44,9 +57,25 @@ func loadSpecPaths(t *testing.T) map[string]string {
4457
if op.OperationID == "" || len(op.Tags) == 0 {
4558
continue
4659
}
47-
ids[op.OperationID] = path
60+
streaming := false
61+
if resp, ok := op.Responses["200"]; ok && len(resp.Content) > 0 {
62+
if _, hasJSON := resp.Content["application/json"]; !hasJSON {
63+
streaming = true
64+
}
65+
}
66+
ops = append(ops, specOpMeta{id: op.OperationID, path: path, streaming: streaming})
4867
}
4968
}
69+
return ops
70+
}
71+
72+
// loadSpecPaths returns operationId -> path for every public GET/POST operation.
73+
func loadSpecPaths(t *testing.T) map[string]string {
74+
t.Helper()
75+
ids := map[string]string{}
76+
for _, op := range loadSpecOps(t) {
77+
ids[op.id] = op.path
78+
}
5079
return ids
5180
}
5281

@@ -110,20 +139,38 @@ func TestEveryOperationHasPathCommand(t *testing.T) {
110139
}
111140

112141
// TestGeneratorTargetsFullSpec asserts the generator emitted a command for every
113-
// spec operation (no gaps, no phantom manifest entries from a stale run).
142+
// non-streaming spec operation (no gaps, no phantom manifest entries from a
143+
// stale run). Streaming ops (200 body is not application/json) are deliberately
144+
// excluded from generation — they cannot be modeled by the typed-response
145+
// template and are served by curated commands instead — so the manifest must NOT
146+
// contain them and they are not required to be generated.
114147
func TestGeneratorTargetsFullSpec(t *testing.T) {
115-
specPaths := loadSpecPaths(t)
148+
ops := loadSpecOps(t)
149+
streaming := map[string]bool{}
150+
wantGenerated := map[string]bool{}
151+
for _, op := range ops {
152+
if op.streaming {
153+
streaming[op.id] = true
154+
continue
155+
}
156+
wantGenerated[op.id] = true
157+
}
158+
116159
gen := map[string]bool{}
117160
for _, id := range generatedOpIDs {
118161
gen[id] = true
119-
if _, ok := specPaths[id]; !ok {
162+
if streaming[id] {
163+
t.Errorf("manifest op %q is streaming and must not be generated (curated only)", id)
164+
}
165+
if !wantGenerated[id] && !streaming[id] {
120166
t.Errorf("manifest op %q is not in the current spec (regenerate cligen)", id)
121167
}
122168
}
123-
for id := range specPaths {
169+
for id := range wantGenerated {
124170
if !gen[id] {
125171
t.Errorf("op %q has no generated command (regenerate cligen)", id)
126172
}
127173
}
128-
t.Logf("generator targets %d/%d spec operations", len(gen), len(specPaths))
174+
t.Logf("generator targets %d/%d non-streaming spec operations (%d streaming, curated)",
175+
len(gen), len(wantGenerated), len(streaming))
129176
}

internal/cli/root.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,21 @@ func init() {
100100
rootCmd.AddCommand(newWhoamiCmd())
101101
rootCmd.AddCommand(newUpdateCmd())
102102

103+
// AI agent sessions (list + transcript export).
104+
rootCmd.AddCommand(newSessionCmd())
105+
103106
// Diagnostics entry points (value-add over the raw API).
104107
rootCmd.AddCommand(newMonitQueryCmd())
105108
rootCmd.AddCommand(newMonitAgentCmd())
106109

107110
// Generated commands (full OpenAPI coverage). Registered AFTER curated
108111
// commands so curated leaves win on any name conflict (see genAddLeaf).
109112
registerGenerated(rootCmd)
113+
114+
// session/export is a streaming op excluded from the generated tree; attach
115+
// its path-is-king leaf to the (now-existing) generated `safari` group so the
116+
// operation stays reachable at safari session-export.
117+
attachSafariSessionExport(rootCmd)
110118
}
111119

112120
// Execute runs the root command.

0 commit comments

Comments
 (0)