Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,46 @@ Configure `OPENAI_API_KEY` in the discrawl repo secrets to enable agent-written

The backup workflows restore and save `.discrawl-ci/discrawl.db` with `actions/cache`. On a warm runner cache, `discrawl update` compares the cached DB's last imported snapshot timestamp with `manifest.json` and skips the full sharded import when they match. Cache misses and newer backup manifests still take the normal pull/import path.

### `digest`

Summarizes per-channel activity for a lookback window.

```bash
discrawl digest
discrawl digest --since 30d
discrawl digest --guild 123456789012345678
discrawl digest --channel general
discrawl --json digest --since 7d --top-n 5
```

Notes:

- `--since` accepts Go durations (`72h`, `30m`) and `Nd` shorthand (`7d`, `30d`)
- `--guild` scopes to one guild; when omitted, `default_guild_id` is used if configured
- `--channel` accepts a channel id or exact channel name
- `--top-n` controls how many top posters and mention targets are shown per channel

### `analytics`

Groups activity-style queries under one namespace.

```bash
discrawl analytics
discrawl analytics digest --since 7d
discrawl analytics quiet --since 30d
discrawl analytics quiet --guild 123456789012345678
discrawl analytics trends --weeks 8
discrawl analytics trends --weeks 12 --channel general
discrawl --json analytics trends --weeks 4
```

Notes:

- `analytics digest` is a shim that delegates to `discrawl digest`
- `analytics quiet` shows channels with no messages in the lookback window, including never-active channels
- `analytics trends` shows week-over-week message counts with zero-filled weekly buckets
- `analytics trends --channel` accepts a channel id or exact channel name

### `doctor`

Checks config, auth, DB, and FTS wiring.
Expand Down
51 changes: 51 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -808,3 +808,54 @@ For an AI agent to finish the product without external memory, this repo should
- milestone order

This file is the authoritative engineering spec for now.

## Digest

`discrawl digest` provides a per-channel activity summary over a lookback window.

Example usage:

```bash
discrawl digest
discrawl digest --since 7d
discrawl digest --since 30d --guild 123456789012345678
discrawl digest --channel general --top-n 5
discrawl --json digest --since 72h
```

Behavior:

- window defaults to `7d` when `--since` is omitted
- `--since` accepts Go durations (`72h`, `30m`) and `Nd` shorthand (`7d`, `30d`)
- `--guild` filters by `guild_id`; empty means no guild filter
- `--channel` accepts channel id or exact channel name
- per-channel metrics include `messages`, `threads` (distinct `reply_to_message_id`), and `active_authors`
- top posters are ranked by message count using member display fallback order: `display_name -> nick -> global_name -> username -> author_id -> unknown`
- top mentions are ranked from `mention_events` and include all target types (`user` and `role`)
- channels are sorted by message count descending, then channel name ascending
- JSON output returns a `Digest` object with channel rows and totals; plain output emits one tab-separated row per channel

## Analytics

`discrawl analytics` is a subcommand group for activity-style queries.

Example usage:

```bash
discrawl analytics
discrawl analytics digest --since 7d
discrawl analytics quiet --since 30d
discrawl analytics quiet --guild 123456789012345678
discrawl analytics trends --weeks 8
discrawl analytics trends --weeks 12 --channel general
discrawl --json analytics trends --weeks 4
```

Behavior:

- `analytics digest` delegates to `discrawl digest` so both paths share one implementation
- `analytics quiet` defaults to `30d` lookback and supports `--guild`
- `analytics quiet` includes channels with no messages at all
- `analytics trends` defaults to `8` weeks and supports `--guild` plus `--channel` (id or exact name)
- `analytics trends` buckets messages into 7-day windows aligned by Unix week boundary and zero-fills missing weeks for every returned channel
- trends rows are sorted by total messages descending, then channel name ascending
103 changes: 103 additions & 0 deletions internal/cli/analytics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package cli

import (
"errors"
"flag"
"fmt"
"io"
"strings"

"github.com/steipete/discrawl/internal/report"
)

func (r *runtime) runAnalytics(args []string) error {
if len(args) == 0 {
printAnalyticsUsage(r.stdout)
return nil
}

subcommand := strings.TrimSpace(args[0])
subArgs := args[1:]
switch subcommand {
case "digest":
return r.runDigest(subArgs)
case "quiet":
return r.runAnalyticsQuiet(subArgs)
case "trends":
return r.runAnalyticsTrends(subArgs)
default:
return usageErr(fmt.Errorf("unknown analytics subcommand %q", subcommand))
}
}

func printAnalyticsUsage(w io.Writer) {
_, _ = fmt.Fprintln(w, "Usage: discrawl analytics <subcommand> [flags]")
_, _ = fmt.Fprintln(w)
_, _ = fmt.Fprintln(w, "Subcommands:")
_, _ = fmt.Fprintln(w, " digest Per-channel activity summary for a window.")
_, _ = fmt.Fprintln(w, " quiet Channels with no activity in the lookback window.")
_, _ = fmt.Fprintln(w, " trends Week-over-week message counts per channel.")
}

func (r *runtime) runAnalyticsQuiet(args []string) error {
fs := flag.NewFlagSet("analytics quiet", flag.ContinueOnError)
fs.SetOutput(io.Discard)
since := fs.String("since", "30d", "")
guild := fs.String("guild", "", "")
if err := fs.Parse(args); err != nil {
return usageErr(err)
}
if fs.NArg() != 0 {
return usageErr(errors.New("analytics quiet takes no positional arguments"))
}

lookback, err := parseLookback(*since)
if err != nil {
return usageErr(fmt.Errorf("parse --since: %w", err))
}
guildID := strings.TrimSpace(*guild)
if guildID == "" {
guildID = r.cfg.EffectiveDefaultGuildID()
}

quiet, err := report.BuildQuiet(r.ctx, r.store, report.QuietOptions{
Since: lookback,
GuildID: guildID,
})
if err != nil {
return err
}
return r.print(quiet)
}

func (r *runtime) runAnalyticsTrends(args []string) error {
fs := flag.NewFlagSet("analytics trends", flag.ContinueOnError)
fs.SetOutput(io.Discard)
weeks := fs.Int("weeks", 8, "")
guild := fs.String("guild", "", "")
channel := fs.String("channel", "", "")
if err := fs.Parse(args); err != nil {
return usageErr(err)
}
if fs.NArg() != 0 {
return usageErr(errors.New("analytics trends takes no positional arguments"))
}
if *weeks < 0 {
return usageErr(errors.New("--weeks must be zero or greater"))
}

guildID := strings.TrimSpace(*guild)
if guildID == "" {
guildID = r.cfg.EffectiveDefaultGuildID()
}

trends, err := report.BuildTrends(r.ctx, r.store, report.TrendsOptions{
Weeks: *weeks,
GuildID: guildID,
Channel: *channel,
})
if err != nil {
return err
}
return r.print(trends)
}
Loading