---
phase: 06-output-reporting
plan: 05
type: execute
wave: 2
depends_on: [06-01, 06-02, 06-03, 06-04]
files_modified:
  - cmd/keys.go
  - cmd/keys_test.go
  - cmd/stubs.go
  - cmd/root.go
autonomous: true
requirements: [KEYS-01, KEYS-02, KEYS-03, KEYS-04, KEYS-05, KEYS-06]

must_haves:
  truths:
    - "keyhunter keys list prints stored findings (masked by default)"
    - "keyhunter keys show <id> prints a single finding in full detail"
    - "keyhunter keys export --format=json|csv writes all findings to stdout or --output file"
    - "keyhunter keys copy <id> places the full key on the system clipboard"
    - "keyhunter keys delete <id> removes a finding with confirmation (bypassed by --yes)"
    - "keyhunter keys verify <id> re-runs HTTPVerifier against the stored key"
  artifacts:
    - path: cmd/keys.go
      provides: "keysCmd + list/show/export/copy/delete/verify subcommands"
      contains: "keysCmd"
  key_links:
    - from: cmd/keys.go
      to: pkg/storage/queries.go
      via: "db.ListFindingsFiltered / GetFinding / DeleteFinding"
      pattern: "ListFindingsFiltered|GetFinding|DeleteFinding"
    - from: cmd/keys.go
      to: pkg/output/formatter.go
      via: "output.Get for json/csv export"
      pattern: "output\\.Get\\("
    - from: cmd/keys.go
      to: github.com/atotto/clipboard
      via: "clipboard.WriteAll for keys copy"
      pattern: "clipboard\\.WriteAll"
    - from: cmd/root.go
      to: cmd/keys.go
      via: "AddCommand(keysCmd)"
      pattern: "AddCommand\\(keysCmd\\)"
---

<objective>
Replace the `keys` stub with a real command tree implementing KEYS-01..06. Reuses the storage query layer from Plan 04 and the formatter registry from Plans 01-03.

Purpose: Fulfils all six key-management requirements.
Output: `cmd/keys.go` with subcommands, removal of stub, tests for list/show/export/delete using in-memory DB.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/06-output-reporting/06-CONTEXT.md
@.planning/phases/06-output-reporting/06-01-PLAN.md
@.planning/phases/06-output-reporting/06-04-PLAN.md
@cmd/scan.go
@cmd/stubs.go
@cmd/root.go
@pkg/storage/findings.go

<interfaces>
From Plan 06-04:
```go
type Filters struct { Provider string; Verified *bool; Limit, Offset int }
func (db *DB) ListFindingsFiltered(encKey []byte, f Filters) ([]Finding, error)
func (db *DB) GetFinding(id int64, encKey []byte) (*Finding, error)
func (db *DB) DeleteFinding(id int64) (int64, error)
```

From cmd/scan.go, reusable helper:
```go
func loadOrCreateEncKey(db *storage.DB, passphrase string) ([]byte, error)
```

From Plan 06-01..03:
```go
output.Get("json"|"csv"|"sarif"|"table") (Formatter, error)
```

clipboard: github.com/atotto/clipboard (already in go.mod) — clipboard.WriteAll(string) error.

verify package (Phase 5): verify.NewHTTPVerifier(timeout), VerifyAll(ctx, findings, reg, workers) — for `keys verify <id>`.
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Implement keys command tree (list/show/export/copy/delete/verify)</name>
  <files>cmd/keys.go, cmd/stubs.go, cmd/root.go</files>
  <read_first>
    - cmd/stubs.go (remove keysCmd stub)
    - cmd/scan.go (loadOrCreateEncKey, db open pattern, verify wiring)
    - cmd/root.go (AddCommand registration)
    - pkg/storage/queries.go (Plan 04 output)
    - pkg/output/formatter.go (Get, Options)
  </read_first>
  <action>
    1. Delete the `keysCmd` stub from cmd/stubs.go (keep the other stubs). Leave a comment if needed.

    2. Create cmd/keys.go with the full command tree. Skeleton:

       ```go
       package cmd

       import (
           "bufio"
           "context"
           "fmt"
           "os"
           "strconv"
           "strings"
           "time"

           "github.com/atotto/clipboard"
           "github.com/salvacybersec/keyhunter/pkg/config"
           "github.com/salvacybersec/keyhunter/pkg/engine"
           "github.com/salvacybersec/keyhunter/pkg/output"
           "github.com/salvacybersec/keyhunter/pkg/providers"
           "github.com/salvacybersec/keyhunter/pkg/storage"
           "github.com/salvacybersec/keyhunter/pkg/verify"
           "github.com/spf13/cobra"
           "github.com/spf13/viper"
       )

       var (
           flagKeysUnmask   bool
           flagKeysProvider string
           flagKeysVerified bool
           flagKeysVerifiedSet bool
           flagKeysLimit    int
           flagKeysFormat   string
           flagKeysOutFile  string
           flagKeysYes      bool
       )

       var keysCmd = &cobra.Command{
           Use:   "keys",
           Short: "Manage stored API key findings",
       }

       // ... list/show/export/copy/delete/verify subcommands below
       ```

    3. Implement subcommands:

       - `keys list`:
         * Flags: --unmask, --provider=string, --verified (tri-state via Changed()), --limit=int
         * Opens DB via same pattern as scan.go; derives encKey via loadOrCreateEncKey.
         * Builds storage.Filters. If cmd.Flag("verified").Changed, set Verified=&flagKeysVerified.
         * Calls db.ListFindingsFiltered.
         * Converts storage.Finding -> engine.Finding (inline helper: storageToEngine(f)).
         * Prepends ID column by printing a preamble line per finding or by printing a compact table. Simplest: iterate and print `[ID] provider confidence masked/unmask source:line verify_status` to stdout. Use lipgloss only if output.ColorsEnabled(os.Stdout).
         * Footer: "N key(s).".
         * Exit code 0 always for list (no findings is not an error).

       - `keys show <id>`:
         * Args: cobra.ExactArgs(1). Parse id as int64.
         * db.GetFinding(id, encKey). If sql.ErrNoRows: "no finding with id N", exit 1.
         * ALWAYS unmasked (per KEYS-02).
         * Print labeled fields: ID, Provider, Confidence, Key (full), Source, Line, SourceType, CreatedAt, Verified, VerifyStatus, VerifyHTTPCode, VerifyMetadata (sorted keys), VerifyError.

       - `keys export`:
         * Flags: --format=json|csv (default json), --output=file (default stdout).
         * Rejects format != "json" && != "csv" with clear error (mentions SARIF is scan-only, for now).
         * Looks up formatter via output.Get(flagKeysFormat). Unmask=true (export implies full keys per KEYS-03).
         * If --output set: atomic write: write to <file>.tmp then os.Rename. Use 0600 perms.
         * Else write to os.Stdout.

       - `keys copy <id>`:
         * Args: ExactArgs(1).
         * GetFinding; if not found exit 1.
         * clipboard.WriteAll(f.KeyValue).
         * Print "Copied key for finding #<id> (<provider>, <masked>) to clipboard."

       - `keys delete <id>`:
         * Args: ExactArgs(1).
         * GetFinding first to show masked preview.
         * If !flagKeysYes: prompt `"Delete finding #%d (%s, %s)? [y/N]: "` reading from stdin (bufio.NewReader). Accept "y"/"Y"/"yes".
         * db.DeleteFinding(id). Print "Deleted finding #<id>." or "No finding with id <id>.".

       - `keys verify <id>`:
         * Args: ExactArgs(1).
         * GetFinding; load providers.NewRegistry(); build one engine.Finding from the stored row.
         * Use verify.EnsureConsent(db, os.Stdin, os.Stderr); if not granted, exit 2.
         * verifier := verify.NewHTTPVerifier(10*time.Second); results := verifier.VerifyAll(ctx, []engine.Finding{f}, reg, 1).
         * Read single result, apply to the stored record (re-SaveFinding with updated verify fields? — simpler: use a new helper `db.UpdateFindingVerify(id, status, httpCode, metadata, errMsg)`; if that helper doesn't exist, do it inline via `db.SQL().Exec("UPDATE findings SET verified=?, verify_status=?, verify_http_code=?, verify_metadata_json=? WHERE id=?", ...)` with JSON-marshaled metadata).
         * Print the updated finding using the "show" rendering.

    4. Helper: `storageToEngine(f storage.Finding) engine.Finding` — maps fields. Put it in cmd/keys.go as unexported.

    5. Helper: `openDBWithKey() (*storage.DB, []byte, error)` that mirrors scan.go's DB-open sequence (config load, mkdir, storage.Open, loadOrCreateEncKey). Extract this so all keys subcommands share one path.

    6. In cmd/root.go the existing `rootCmd.AddCommand(keysCmd)` line already registers the stub's keysCmd. Since cmd/keys.go now declares `var keysCmd`, ensure the old declaration in cmd/stubs.go is removed (Task 1 step 1) so there is exactly one declaration. Run `go build ./cmd/...` to confirm.

    7. Register subcommands in an init() in cmd/keys.go:
       ```go
       func init() {
           // list
           keysListCmd.Flags().BoolVar(&flagKeysUnmask, "unmask", false, "show full key values")
           keysListCmd.Flags().StringVar(&flagKeysProvider, "provider", "", "filter by provider name")
           keysListCmd.Flags().BoolVar(&flagKeysVerified, "verified", false, "filter: verified only (use --verified=false for unverified only)")
           keysListCmd.Flags().IntVar(&flagKeysLimit, "limit", 0, "max rows (0 = unlimited)")
           // export
           keysExportCmd.Flags().StringVar(&flagKeysFormat, "format", "json", "export format: json, csv")
           keysExportCmd.Flags().StringVar(&flagKeysOutFile, "output", "", "write to file instead of stdout")
           // delete
           keysDeleteCmd.Flags().BoolVar(&flagKeysYes, "yes", false, "skip confirmation")
           // wiring
           keysCmd.AddCommand(keysListCmd, keysShowCmd, keysExportCmd, keysCopyCmd, keysDeleteCmd, keysVerifyCmd)
           _ = viper.BindPFlag("keys.unmask", keysListCmd.Flags().Lookup("unmask"))
       }
       ```
  </action>
  <verify>
    <automated>cd /home/salva/Documents/apikey && go build ./... && go vet ./cmd/...</automated>
  </verify>
  <done>
    - `go build ./...` succeeds
    - `cmd/stubs.go` no longer declares keysCmd
    - `cmd/keys.go` declares keysCmd + 6 subcommands
    - `grep -q "keysListCmd\|keysShowCmd\|keysExportCmd\|keysCopyCmd\|keysDeleteCmd\|keysVerifyCmd" cmd/keys.go`
    - `keyhunter keys --help` (via `go run ./ keys --help`) lists all 6 subcommands
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Integration tests for keys list/show/export/delete against in-memory DB</name>
  <files>cmd/keys_test.go</files>
  <read_first>
    - cmd/keys.go (from Task 1)
    - pkg/storage/queries.go
  </read_first>
  <behavior>
    - Tests use a temp file SQLite DB (not :memory: because cobra commands open by path).
    - Each test sets viper.Set("database.path", tmpPath) and config passphrase via env, seeds findings, then invokes the cobra subcommand via rootCmd.SetArgs() + Execute() OR directly invokes the RunE function with captured stdout.
    - Prefer direct RunE invocation with cmd.SetOut/SetErr buffers to isolate from global os.Stdout.
    - Seed: 3 findings (2 openai, 1 anthropic; one verified).
    - Tests:
      * TestKeysList_Default: output contains both providers and all 3 ids, key column masked.
      * TestKeysList_FilterProvider: --provider=openai shows only 2 rows.
      * TestKeysShow_Hit: `keys show <id1>` output contains the full plaintext KeyValue, not masked.
      * TestKeysShow_Miss: `keys show 9999` returns a non-nil error.
      * TestKeysExport_JSON: --format=json to stdout parses as JSON array of length 3, unmasked keys present.
      * TestKeysExport_CSVFile: --format=csv --output=<tmp>; file exists, header row matches, 3 data rows.
      * TestKeysDelete_WithYes: --yes deletes finding; subsequent list returns 2.
    - Skip TestKeysCopy (clipboard not available in test env) and TestKeysVerify (requires network). Document skip with a comment.
  </behavior>
  <action>
    Create cmd/keys_test.go using testify. Use t.TempDir() for DB path. Seed findings with a helper similar to storage/queries_test. For each test, reset viper state and flag vars between runs.

    Example scaffold:
    ```go
    func seedDB(t *testing.T) (string, func()) {
        t.Helper()
        dir := t.TempDir()
        dbPath := filepath.Join(dir, "k.db")
        viper.Set("database.path", dbPath)
        t.Setenv("KEYHUNTER_PASSPHRASE", "test-pass")
        db, err := storage.Open(dbPath)
        require.NoError(t, err)
        encKey, err := loadOrCreateEncKey(db, "test-pass")
        require.NoError(t, err)
        seed := []storage.Finding{
            {ProviderName: "openai", KeyValue: "sk-aaaaaaaaaaaaaaaaaaaa", KeyMasked: "sk-aaaa...aaaa", Confidence: "high", SourcePath: "a.go", LineNumber: 10},
            {ProviderName: "openai", KeyValue: "sk-bbbbbbbbbbbbbbbbbbbb", KeyMasked: "sk-bbbb...bbbb", Confidence: "medium", SourcePath: "b.go", LineNumber: 20, Verified: true, VerifyStatus: "live"},
            {ProviderName: "anthropic", KeyValue: "sk-ant-cccccccccccccccc", KeyMasked: "sk-ant-c...cccc", Confidence: "high", SourcePath: "c.go", LineNumber: 30},
        }
        for _, f := range seed {
            _, err := db.SaveFinding(f, encKey)
            require.NoError(t, err)
        }
        require.NoError(t, db.Close())
        return dbPath, func() { viper.Reset() }
    }
    ```

    Capture output by using `cmd.SetOut(buf); cmd.SetErr(buf)` then `cmd.Execute()` on a copy of keysCmd, OR directly call `keysListCmd.RunE(keysListCmd, []string{})` after redirecting `os.Stdout` to a pipe (prefer SetOut if subcommands write via `cmd.OutOrStdout()`; update keys.go to use that helper in Task 1 so tests are clean).

    NOTE: If Task 1 wrote fmt.Fprintln(os.Stdout, ...), adjust it to use `cmd.OutOrStdout()` to make these tests hermetic. This is a cheap refactor — do it during Task 2 if missed in Task 1.
  </action>
  <verify>
    <automated>cd /home/salva/Documents/apikey && go test ./cmd/... -run "TestKeysList|TestKeysShow|TestKeysExport|TestKeysDelete" -count=1</automated>
  </verify>
  <done>
    - All listed keys tests pass
    - `go test ./cmd/... -count=1` has no regressions
    - Test file uses cmd.OutOrStdout() pattern (cmd/keys.go updated if needed)
  </done>
</task>

</tasks>

<verification>
- `go build ./...` succeeds
- `go test ./cmd/... ./pkg/storage/... ./pkg/output/... -count=1` all green
- Manual smoke: `go run . keys --help` lists list/show/export/copy/delete/verify
</verification>

<success_criteria>
- All of KEYS-01..06 are implemented
- keys export reuses the formatter registry (JSON/CSV)
- keys copy uses atotto/clipboard
- keys delete requires confirmation unless --yes
- Integration tests cover list/show/export/delete
</success_criteria>

<output>
After completion, create `.planning/phases/06-output-reporting/06-05-SUMMARY.md`.
</output>
