--- phase: 04-power-user-features plan: 02 type: execute wave: 2 depends_on: [04-01] files_modified: - cmd/netsynth/main.go - cmd/netsynth/main_test.go autonomous: true requirements: [CAPT-05, CAPT-06] must_haves: truths: - "User can run netsynth -i eth0 --filter 'port 53' and only matching traffic is captured" - "User can run netsynth --read capture.pcap -o out.mp3 and receive a valid MP3" - "An invalid BPF filter produces a clear error before any capture begins" - "--read and -i are mutually exclusive with a clear error message" - "--read without -o derives output filename from input pcap (capture.pcap -> capture.mp3)" - "--read displays bookend messages: 'Reading ...' at start, summary + 'Saved' at end" - "--verbose works with --read showing per-window activity" artifacts: - path: "cmd/netsynth/main.go" provides: "CLI wiring for --filter and --read flags with branching run logic" contains: ["--filter", "--read", "ReadPcapFile", "AggregatePcap", "deriveOutputPath"] key_links: - from: "cmd/netsynth/main.go" to: "capture/bpf.go" via: "ValidateBPFFilter call before capture" pattern: "capture\\.ValidateBPFFilter" - from: "cmd/netsynth/main.go" to: "capture/pcap_reader.go" via: "ReadPcapFile for --read mode" pattern: "capture\\.ReadPcapFile" - from: "cmd/netsynth/main.go" to: "aggregate/window.go" via: "AggregatePcap for pcap mode" pattern: "aggregate\\.AggregatePcap" --- Wire --filter and --read flags into the CLI, branching run() into live and pcap paths. Purpose: Complete the user-facing features CAPT-05 and CAPT-06 by connecting the library functions from Plan 01 into the Cobra CLI. Output: Updated cmd/netsynth/main.go with both flags, mutual exclusion, filename derivation, bookend messages, and pcap processing path. Tests for flag interactions. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/04-power-user-features/04-CONTEXT.md @.planning/phases/04-power-user-features/04-RESEARCH.md @.planning/phases/04-power-user-features/04-01-SUMMARY.md @cmd/netsynth/main.go @cmd/netsynth/main_test.go From capture/bpf.go (created in Plan 01): ```go func ValidateBPFFilter(expr string) error func CompileSoftwareBPF(expr string) (*bpf.VM, error) ``` From capture/pcap_reader.go (created in Plan 01): ```go func ReadPcapFile(path string, filter string) (<-chan gopacket.Packet, error) ``` From capture/capture.go (updated in Plan 01): ```go func OpenCapture(ctx context.Context, iface string, filter string) (*pcap.Handle, error) func StartCapture(ctx context.Context, iface string, filter string) (<-chan gopacket.Packet, *int64, error) ``` From aggregate/window.go (updated in Plan 01): ```go func AggregatePcap(events <-chan classify.ClassifiedPacket, windowMs int, onSnapshot func(classify.WindowSnapshot)) []classify.WindowSnapshot ``` From classify/types.go (updated in Plan 01): ```go type ClassifiedPacket struct { Class TrafficClass SrcPort uint16 DstPort uint16 Protocol string Length int Timestamp time.Time } ``` Existing from cmd/netsynth/main.go: ```go var ( ifaceName string listIfaces bool verbose bool outputPath string ) func run(cmd *cobra.Command, args []string) error ``` Task 1: Add --filter and --read flags with branching run logic cmd/netsynth/main.go, cmd/netsynth/main_test.go cmd/netsynth/main.go, cmd/netsynth/main_test.go, capture/bpf.go, capture/pcap_reader.go, aggregate/window.go - TestFlagMutualExclusion: Running with both --read and -i returns error containing "mutually exclusive" - TestMissingSource: Running without --read and without -i returns error containing "interface required" and mentions "--read" - TestDeriveOutputPath: deriveOutputPath("capture.pcap") returns "capture.mp3"; deriveOutputPath("/tmp/net.pcap") returns "/tmp/net.mp3"; deriveOutputPath("noext") returns "noext.mp3" - TestFilterFlagRegistered: The root command has a --filter flag registered - TestReadFlagRegistered: The root command has a --read flag registered - TestInvalidBPFFilter: Running with -i lo --filter "invalid garbage xyz" returns error containing "invalid BPF filter" (no capture started) - TestHelpOutputNewFlags: --help output contains "--filter" and "--read" **1. Add new global variables and flag registration in main():** ```go var ( ifaceName string listIfaces bool verbose bool outputPath string bpfFilter string // NEW: --filter flag readPath string // NEW: --read flag ) ``` In `main()`, add after existing flag registrations: ```go rootCmd.Flags().StringVar(&bpfFilter, "filter", "", "BPF filter expression (tcpdump syntax, e.g. \"port 53\")") rootCmd.Flags().StringVar(&readPath, "read", "", "Read packets from pcap file instead of live capture") ``` **2. Add deriveOutputPath helper function:** ```go // deriveOutputPath replaces the file extension with .mp3 per D-05. // "capture.pcap" -> "capture.mp3", "noext" -> "noext.mp3" func deriveOutputPath(readPath string) string { ext := filepath.Ext(readPath) if ext == "" { return readPath + ".mp3" } return strings.TrimSuffix(readPath, ext) + ".mp3" } ``` Add `"path/filepath"` to imports. **3. Rewrite run() with branching logic:** At the top of `run()`, after the `--list-interfaces` check: ```go // D-03: --read and -i are mutually exclusive if readPath != "" && ifaceName != "" { return fmt.Errorf("--read and -i are mutually exclusive; use one or the other") } if readPath == "" && ifaceName == "" { return fmt.Errorf("interface required: use -i , --read , or --list-interfaces") } // Pre-validate BPF filter before any capture (CAPT-05, Pitfall 1) if bpfFilter != "" { if err := capture.ValidateBPFFilter(bpfFilter); err != nil { return err } } // Resolve output path if outputPath == "" { if readPath != "" { outputPath = deriveOutputPath(readPath) // D-05 } else { outputPath = fmt.Sprintf("netsynth-%s.mp3", time.Now().Format("20060102-150405")) } } ``` Then branch into live vs pcap mode: ```go if readPath != "" { return runPcapMode(cmd) } return runLiveMode(cmd) ``` **4. Extract existing live capture logic into runLiveMode(cmd):** Move the existing pipeline code (signal handling, StartCapture, classify goroutine, Aggregate, snapshot collection, summary, encoding, saved message) into `func runLiveMode(cmd *cobra.Command) error`. Pass `bpfFilter` to `capture.StartCapture(ctx, ifaceName, bpfFilter)`. **5. Create runPcapMode(cmd):** ```go func runPcapMode(cmd *cobra.Command) error { // D-06: bookend start message fmt.Fprintf(os.Stderr, "Reading %s...\n", readPath) // Open pcap file with optional software BPF filter (D-04) packets, err := capture.ReadPcapFile(readPath, bpfFilter) if err != nil { return err } // Classify packets (reuse same classifier) classifier := classify.NewClassifier(classify.DefaultRules) classified := make(chan classify.ClassifiedPacket, 1024) go func() { defer close(classified) for pkt := range packets { cp := classifier.Classify(pkt) // Set timestamp from pcap metadata for D-01 window assignment cp.Timestamp = pkt.Metadata().CaptureInfo.Timestamp classified <- cp } }() // D-07: --verbose callback (same as live mode) var onSnapshot func(classify.WindowSnapshot) if verbose { onSnapshot = func(snap classify.WindowSnapshot) { aggregate.PrintWindowLine(os.Stderr, snap) } } // Timestamp-based aggregation (D-01, D-02) collectedSnapshots := aggregate.AggregatePcap(classified, aggregate.DefaultWindowMs, onSnapshot) // Accumulate totals for summary totals := make(map[classify.TrafficClass]int64) for _, snap := range collectedSnapshots { aggregate.AccumulateTotals(totals, snap) } // D-06: protocol summary (same format as live mode) aggregate.PrintSummary(os.Stderr, totals) // Check for empty pcap (Pitfall 4: better error than generic "no packets captured") if len(collectedSnapshots) == 0 { return fmt.Errorf("pcap file %q contains no packets (after filtering)", readPath) } // Encode fmt.Fprintf(os.Stderr, "Encoding %d windows to %s...\n", len(collectedSnapshots), outputPath) encodeStart := time.Now() if err := encode.RunSynthesis(collectedSnapshots, outputPath); err != nil { return fmt.Errorf("synthesis failed: %w", err) } encodeElapsed := time.Since(encodeStart) // D-06: bookend end message audioDuration := float64(len(collectedSnapshots)) * float64(aggregate.DefaultWindowMs) / 1000.0 info, statErr := os.Stat(outputPath) if statErr != nil { return fmt.Errorf("stat output file: %w", statErr) } fmt.Fprintf(os.Stderr, "Saved %s (%.1fs, %d KB, encoded in %.1fs)\n", outputPath, audioDuration, info.Size()/1024, encodeElapsed.Seconds()) return nil } ``` **6. Update tests in cmd/netsynth/main_test.go:** Add new test functions per behavior list. Update `newTestCmd()` helper to include the new `--filter` and `--read` flags wired to the global `bpfFilter` and `readPath` variables. Tests for mutual exclusion and deriveOutputPath are pure logic tests (no privileges needed). The invalid BPF filter test uses `-i lo --filter "invalid garbage xyz"` — the BPF validation runs before capture starts, so it returns error without needing capture privileges. Update the existing `TestMissingInterfaceFlag` test to also verify the error message now mentions `--read`. cd /home/dev/workspace/yoloyolo && export PATH="/home/dev/tools/go-install/go/bin:$PATH" && go test ./cmd/netsynth/... -run "TestFlagMutualExclusion|TestMissingSource|TestDeriveOutputPath|TestFilterFlag|TestReadFlag|TestInvalidBPF|TestHelpOutputNewFlags" -v -count=1 && go build ./... - cmd/netsynth/main.go contains `rootCmd.Flags().StringVar(&bpfFilter, "filter",` - cmd/netsynth/main.go contains `rootCmd.Flags().StringVar(&readPath, "read",` - cmd/netsynth/main.go contains `"--read and -i are mutually exclusive"` - cmd/netsynth/main.go contains `func deriveOutputPath(readPath string) string` - cmd/netsynth/main.go contains `capture.ValidateBPFFilter(bpfFilter)` - cmd/netsynth/main.go contains `capture.ReadPcapFile(readPath, bpfFilter)` - cmd/netsynth/main.go contains `aggregate.AggregatePcap(classified, aggregate.DefaultWindowMs` - cmd/netsynth/main.go contains `cp.Timestamp = pkt.Metadata().CaptureInfo.Timestamp` - cmd/netsynth/main.go contains `fmt.Fprintf(os.Stderr, "Reading %s...\n", readPath)` (D-06) - cmd/netsynth/main.go contains `capture.StartCapture(ctx, ifaceName, bpfFilter)` - cmd/netsynth/main.go contains `deriveOutputPath(readPath)` (D-05) - cmd/netsynth/main.go contains `"pcap file %q contains no packets"` (Pitfall 4) - cmd/netsynth/main_test.go contains `TestFlagMutualExclusion` - cmd/netsynth/main_test.go contains `TestDeriveOutputPath` - `go test ./cmd/netsynth/... -v` passes all new tests - `go build ./...` succeeds - `go test ./... -count=1` all green (full suite) Both --filter and --read flags are wired into the CLI. Mutual exclusion validated (D-03). BPF filter pre-validated before capture (CAPT-05). Pcap mode reads file, classifies with timestamps, aggregates with AggregatePcap, encodes MP3 (CAPT-06). Output filename derived from input (D-05). Bookend messages displayed (D-06). --verbose works in pcap mode (D-07). All tests pass, all packages compile. Full suite verification: ```bash cd /home/dev/workspace/yoloyolo && export PATH="/home/dev/tools/go-install/go/bin:$PATH" && go test ./... -count=1 -v && go build ./... ``` Phase success criteria from ROADMAP: 1. `netsynth -i eth0 --filter "port 53"` — filter applied to live capture (requires privileges to test live) 2. `netsynth --read capture.pcap -o out.mp3` — pcap mode produces valid MP3 (testable without privileges) 3. Invalid BPF filter produces clear error before capture — verified by unit test 1. `--filter` flag registered and functional: pre-validates BPF, applies to live handle, applies software BPF to pcap 2. `--read` flag registered and functional: reads pcap, classifies with timestamps, aggregates, encodes MP3 3. `--read` and `-i` mutually exclusive with clear error 4. `--read` without `-o` derives output from input filename (D-05) 5. Bookend messages: "Reading ..." at start, summary + "Saved" at end (D-06) 6. `--verbose` works with `--read` (D-07) 7. Invalid BPF filter errors before capture (CAPT-05) 8. Empty pcap file produces clear error with file path (Pitfall 4) 9. `go test ./... -count=1` all green 10. `go build ./...` succeeds After completion, create `.planning/phases/04-power-user-features/04-02-SUMMARY.md`