From 912265cc250232a0c1a173371e3bacacd388be59 Mon Sep 17 00:00:00 2001 From: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:47:44 +0000 Subject: [PATCH 1/3] cli: improve command help output --- internal/cli/cli.go | 133 +++++++++++++++++++++++++++++++++++---- internal/cli/cli_test.go | 55 +++++++++++++++- internal/cli/output.go | 59 ++++------------- 3 files changed, 188 insertions(+), 59 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 0ef849e..b03550f 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -49,8 +49,7 @@ func ExitCode(err error) int { func Run(ctx context.Context, args []string, stdout, stderr io.Writer) error { if len(args) == 0 || rootHelpRequested(args, "config") { - printUsage(stdout) - return nil + return printUsage(stdout) } var global discrawlRootArgs if err := parseKongArgs(&global, args, "discrawl", stdout, stderr); err != nil { @@ -62,12 +61,14 @@ func Run(ctx context.Context, args []string, stdout, stderr io.Writer) error { } rest := global.Args if len(rest) == 0 || rest[0] == "--help" || rest[0] == "-h" || (rest[0] == "help" && len(rest) == 1) { - printUsage(stdout) - return nil + return printUsage(stdout) } if rest[0] == "help" { return printCommandUsage(stdout, rest[1:]) } + if rest[0] != "tui" && hasHelpFlag(rest[1:]) { + return printCommandUsage(stdout, commandHelpTopic(rest)) + } if rest[0] == "version" { _, _ = io.WriteString(stdout, version+"\n") return nil @@ -92,15 +93,123 @@ func Run(ctx context.Context, args []string, stdout, stderr io.Writer) error { return runtime.dispatch(rest) } +type discrawlGlobalArgs struct { + Config string `help:"Config path."` + JSON bool `name:"json" help:"Write JSON output."` + Plain bool `help:"Write stable plain text output when available."` + Quiet bool `short:"q" help:"Only log errors."` + Verbose bool `short:"v" help:"Enable debug logging."` + Version bool `help:"Print version and exit."` + NoColor bool `name:"no-color" help:"Disable color output."` +} + type discrawlRootArgs struct { - Config string `help:"Config path."` - JSON bool `name:"json" help:"Write JSON output."` - Plain bool `help:"Write stable plain text output when available."` - Quiet bool `short:"q" help:"Only log errors."` - Verbose bool `short:"v" help:"Enable debug logging."` - Version bool `help:"Print version and exit."` - NoColor bool `name:"no-color" help:"Disable color output."` - Args []string `arg:"" optional:"" passthrough:"partial" name:"command" help:"Command and arguments."` + discrawlGlobalArgs + Args []string `arg:"" optional:"" passthrough:"partial" name:"command" help:"Command and arguments."` +} + +type discrawlHelpArgs struct { + discrawlGlobalArgs +} + +type discrawlAnalyticsHelpArgs struct { + Quiet struct{} `cmd:"" help:"List channels with no activity in the lookback window."` + Trends struct{} `cmd:"" help:"Report week-over-week message counts per channel."` +} + +type discrawlCommandSpec struct { + name string + description string +} + +var discrawlCommandSpecs = []discrawlCommandSpec{ + {name: "metadata", description: "Print archive snapshot metadata."}, + {name: "check-update", description: "Check GitHub Releases for a newer Discrawl build."}, + {name: "version", description: "Print the Discrawl version."}, + {name: "init", description: "Initialize Discrawl configuration."}, + {name: "sync", description: "Sync Discord data into the local archive."}, + {name: "tail", description: "Continuously archive new Discord messages."}, + {name: "tap", description: "Import Discord Desktop cache data (wiretap alias)."}, + {name: "cache-import", description: "Import Discord Desktop cache data (wiretap alias)."}, + {name: "wiretap", description: "Import Discord Desktop cache data."}, + {name: "search", description: "Search archived messages."}, + {name: "tui", description: "Explore the archive in an interactive terminal UI."}, + {name: "messages", description: "List archived messages."}, + {name: "digest", description: "Summarize recent archive activity."}, + {name: "analytics", description: "Analyze archive activity and trends."}, + {name: "dms", description: "List local Discord Desktop conversations."}, + {name: "mentions", description: "List archived mentions."}, + {name: "attachments", description: "List or fetch archived attachments."}, + {name: "embed", description: "Generate embeddings for archived messages."}, + {name: "sql", description: "Run SQL queries against the local archive."}, + {name: "members", description: "Inspect archived Discord members."}, + {name: "channels", description: "Inspect and resolve archived channels."}, + {name: "status", description: "Show archive status and freshness."}, + {name: "diagnostics", description: "Report SQLite and sync-lock diagnostics."}, + {name: "coverage", description: "Report archive coverage."}, + {name: "failures", description: "List retained sync and import failures."}, + {name: "remote", description: "Access a configured remote archive."}, + {name: "whoami", description: "Show the configured remote identity."}, + {name: "report", description: "Generate archive reports."}, + {name: "publish", description: "Publish the configured archive snapshot."}, + {name: "doctor", description: "Check Discrawl configuration and dependencies."}, + {name: "cloud", description: "Manage a Cloudflare-backed remote archive."}, + {name: "subscribe", description: "Configure a read-only snapshot subscription."}, + {name: "subscribe-cloud", description: "Configure a read-only cloud archive."}, + {name: "update", description: "Update the configured archive snapshot."}, +} + +func newDiscrawlHelpParser(stdout io.Writer) (*kong.Kong, error) { + var root discrawlHelpArgs + var command struct{} + var analytics discrawlAnalyticsHelpArgs + options := []kong.Option{ + kong.Name("discrawl"), + kong.Description("discrawl archives Discord guild data into local SQLite."), + kong.Writers(stdout, io.Discard), + kong.Exit(func(int) {}), + kong.ConfigureHelp(kong.HelpOptions{Compact: true, NoExpandSubcommands: true}), + } + for _, spec := range discrawlCommandSpecs { + target := any(&command) + if spec.name == "analytics" { + target = &analytics + } + options = append(options, kong.DynamicCommand(spec.name, spec.description, "", target)) + } + return kong.New(&root, options...) +} + +func printKongUsage(stdout io.Writer, command string) error { + parser, err := newDiscrawlHelpParser(stdout) + if err != nil { + return err + } + args := []string{"--help"} + if command != "" { + args = append(strings.Fields(command), "--help") + } + _, _ = parser.Parse(args) + return nil +} + +func hasHelpTopic(args []string) bool { + if len(args) == 1 { + for _, spec := range discrawlCommandSpecs { + if spec.name == args[0] { + return true + } + } + return false + } + return len(args) == 2 && args[0] == "analytics" && (args[1] == "quiet" || args[1] == "trends") +} + +func commandHelpTopic(rest []string) []string { + if len(rest) >= 2 && hasHelpTopic(rest[:2]) { + return rest[:2] + } + return rest[:1] } func rootHelpRequested(args []string, valueFlags ...string) bool { diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index cb6dfde..da11de9 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -61,6 +61,20 @@ func TestHelpAndVersion(t *testing.T) { require.Equal(t, 7, ExitCode(&cliError{code: 7, err: errors.New("custom")})) } +func TestTopLevelHelpIncludesDescriptions(t *testing.T) { + t.Parallel() + + for _, helpFlag := range []string{"-h", "--help"} { + var stdout, stderr bytes.Buffer + require.NoError(t, Run(context.Background(), []string{helpFlag}, &stdout, &stderr)) + require.Contains(t, stdout.String(), "wiretap") + require.Contains(t, stdout.String(), "Import Discord Desktop cache data.") + require.Contains(t, stdout.String(), "search") + require.Contains(t, stdout.String(), "Search archived messages.") + require.Empty(t, stderr.String()) + } +} + func TestCommandValidationEdges(t *testing.T) { ctx := context.Background() dir := t.TempDir() @@ -4051,14 +4065,25 @@ func TestCommandHelpDoesNotOpenConfigOrStore(t *testing.T) { for _, args := range [][]string{ {"--config", filepath.Join(t.TempDir(), "missing.toml"), "help", "search"}, + {"--config", filepath.Join(t.TempDir(), "missing.toml"), "help", "wiretap"}, {"--config", filepath.Join(t.TempDir(), "missing.toml"), "search", "--help"}, {"--config", filepath.Join(t.TempDir(), "missing.toml"), "messages", "--help"}, {"--config", filepath.Join(t.TempDir(), "missing.toml"), "sql", "--help"}, {"--config", filepath.Join(t.TempDir(), "missing.toml"), "coverage", "--help"}, + {"--config", filepath.Join(t.TempDir(), "missing.toml"), "wiretap", "-h"}, + {"--config", filepath.Join(t.TempDir(), "missing.toml"), "wiretap", "--help"}, } { var stdout, stderr bytes.Buffer require.NoError(t, Run(context.Background(), args, &stdout, &stderr), "args=%v", args) - require.Contains(t, stdout.String(), "Usage:", "args=%v", args) + require.Contains(t, stdout.String(), "Usage", "args=%v", args) + require.Empty(t, stderr.String(), "args=%v", args) + } + + for _, spec := range discrawlCommandSpecs { + args := []string{"--config", filepath.Join(t.TempDir(), "missing.toml"), spec.name, "--help"} + var stdout, stderr bytes.Buffer + require.NoError(t, Run(context.Background(), args, &stdout, &stderr), "args=%v", args) + require.Contains(t, stdout.String(), "Usage", "args=%v", args) require.Empty(t, stderr.String(), "args=%v", args) } @@ -4067,6 +4092,34 @@ func TestCommandHelpDoesNotOpenConfigOrStore(t *testing.T) { require.Contains(t, err.Error(), `unknown help topic "wat"`) } +func TestNestedCommandHelp(t *testing.T) { + t.Parallel() + + for _, args := range [][]string{ + {"analytics", "-h"}, + {"analytics", "--help"}, + {"help", "analytics"}, + } { + var stdout, stderr bytes.Buffer + require.NoError(t, Run(context.Background(), args, &stdout, &stderr), "args=%v", args) + require.Contains(t, stdout.String(), "Usage: discrawl analytics [flags]", "args=%v", args) + require.Contains(t, stdout.String(), "quiet", "args=%v", args) + require.Contains(t, stdout.String(), "trends", "args=%v", args) + require.Empty(t, stderr.String(), "args=%v", args) + } + + for _, args := range [][]string{ + {"analytics", "quiet", "--help"}, + {"help", "analytics", "quiet"}, + } { + var stdout, stderr bytes.Buffer + require.NoError(t, Run(context.Background(), args, &stdout, &stderr), "args=%v", args) + require.Contains(t, stdout.String(), "Usage: discrawl analytics quiet", "args=%v", args) + require.Contains(t, stdout.String(), "no activity in the lookback window", "args=%v", args) + require.Empty(t, stderr.String(), "args=%v", args) + } +} + func TestHelpers(t *testing.T) { t.Parallel() diff --git a/internal/cli/output.go b/internal/cli/output.go index 083fedb..e76e10f 100644 --- a/internal/cli/output.go +++ b/internal/cli/output.go @@ -95,57 +95,24 @@ func printPlain(w io.Writer, value any) error { } } -func printUsage(w io.Writer) { - _, _ = fmt.Fprint(w, `discrawl archives Discord guild data into local SQLite. - -Usage: - discrawl [global flags] [args] - -Commands: - metadata - check-update - version - init - sync - tail - tap - cache-import - wiretap - search - tui - messages - digest - analytics - dms - mentions - attachments - embed - sql - members - channels - status - diagnostics - coverage - failures - remote - whoami - report - doctor - cloud - subscribe-cloud -`) +func printUsage(w io.Writer) error { + return printKongUsage(w, "") } func printCommandUsage(w io.Writer, args []string) error { - if len(args) != 1 { - return usageErr(errors.New("usage: discrawl help ")) + if len(args) == 0 || len(args) > 2 { + return usageErr(errors.New("usage: discrawl help [subcommand]")) } - text, ok := commandUsage[args[0]] - if !ok { - return usageErr(fmt.Errorf("unknown help topic %q", args[0])) + topic := strings.Join(args, " ") + text, ok := commandUsage[topic] + if ok { + _, _ = fmt.Fprint(w, text) + return nil } - _, _ = fmt.Fprint(w, text) - return nil + if !hasHelpTopic(args) { + return usageErr(fmt.Errorf("unknown help topic %q", topic)) + } + return printKongUsage(w, topic) } var commandUsage = map[string]string{ From bc0d22bc80ada6c672a161c21d68c22dfd67286c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 16 Jul 2026 11:13:14 -0700 Subject: [PATCH 2/3] fix(cli): honor help delimiter --- CHANGELOG.md | 4 ++++ internal/cli/cli_test.go | 11 +++++++++++ internal/cli/query_sync.go | 3 +++ 3 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8fbe2..49ef4cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 0.11.6 - Unreleased +### Changes + +- Improve root, command, and nested command help with descriptions and side-effect-free `-h` handling. Thanks @0xdevalias. + ### Fixes - Show setup guidance instead of a raw missing-file error when configuration-dependent commands cannot find `config.toml`. Thanks @0xdevalias. diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index da11de9..cbd427b 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -4120,6 +4120,17 @@ func TestNestedCommandHelp(t *testing.T) { } } +func TestHelpFlagAfterDelimiterReachesCommand(t *testing.T) { + t.Parallel() + + for _, helpFlag := range []string{"-h", "--help"} { + var stdout, stderr bytes.Buffer + require.NoError(t, Run(context.Background(), []string{"version", "--", helpFlag}, &stdout, &stderr)) + require.Equal(t, version+"\n", stdout.String()) + require.Empty(t, stderr.String()) + } +} + func TestHelpers(t *testing.T) { t.Parallel() diff --git a/internal/cli/query_sync.go b/internal/cli/query_sync.go index 788da2c..a89a0d7 100644 --- a/internal/cli/query_sync.go +++ b/internal/cli/query_sync.go @@ -107,6 +107,9 @@ func hasHelpArg(args []string) bool { func hasHelpFlag(args []string) bool { for _, arg := range args { + if arg == "--" { + return false + } if arg == "--help" || arg == "-h" { return true } From e584bb6203fbfc73164723fefa4d84d8a7287380 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 16 Jul 2026 11:53:21 -0700 Subject: [PATCH 3/3] fix(cli): preserve detailed command help --- internal/cli/cli.go | 56 +++++++-------- internal/cli/cli_test.go | 24 ++++++- internal/cli/output.go | 143 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 183 insertions(+), 40 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index b03550f..f7f9e3f 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -66,8 +66,8 @@ func Run(ctx context.Context, args []string, stdout, stderr io.Writer) error { if rest[0] == "help" { return printCommandUsage(stdout, rest[1:]) } - if rest[0] != "tui" && hasHelpFlag(rest[1:]) { - return printCommandUsage(stdout, commandHelpTopic(rest)) + if topic, ok := earlyCommandHelpTopic(rest); ok { + return printCommandUsage(stdout, topic) } if rest[0] == "version" { _, _ = io.WriteString(stdout, version+"\n") @@ -112,11 +112,6 @@ type discrawlHelpArgs struct { discrawlGlobalArgs } -type discrawlAnalyticsHelpArgs struct { - Quiet struct{} `cmd:"" help:"List channels with no activity in the lookback window."` - Trends struct{} `cmd:"" help:"Report week-over-week message counts per channel."` -} - type discrawlCommandSpec struct { name string description string @@ -162,7 +157,6 @@ var discrawlCommandSpecs = []discrawlCommandSpec{ func newDiscrawlHelpParser(stdout io.Writer) (*kong.Kong, error) { var root discrawlHelpArgs var command struct{} - var analytics discrawlAnalyticsHelpArgs options := []kong.Option{ kong.Name("discrawl"), kong.Description("discrawl archives Discord guild data into local SQLite."), @@ -171,45 +165,45 @@ func newDiscrawlHelpParser(stdout io.Writer) (*kong.Kong, error) { kong.ConfigureHelp(kong.HelpOptions{Compact: true, NoExpandSubcommands: true}), } for _, spec := range discrawlCommandSpecs { - target := any(&command) - if spec.name == "analytics" { - target = &analytics - } - options = append(options, kong.DynamicCommand(spec.name, spec.description, "", target)) + options = append(options, kong.DynamicCommand(spec.name, spec.description, "", &command)) } return kong.New(&root, options...) } -func printKongUsage(stdout io.Writer, command string) error { +func printKongUsage(stdout io.Writer) error { parser, err := newDiscrawlHelpParser(stdout) if err != nil { return err } - args := []string{"--help"} - if command != "" { - args = append(strings.Fields(command), "--help") - } - _, _ = parser.Parse(args) + _, _ = parser.Parse([]string{"--help"}) return nil } -func hasHelpTopic(args []string) bool { - if len(args) == 1 { - for _, spec := range discrawlCommandSpecs { - if spec.name == args[0] { - return true - } +func earlyCommandHelpTopic(rest []string) ([]string, bool) { + if len(rest) < 2 || rest[0] == "tui" || !hasHelpFlag(rest[1:]) { + return nil, false + } + if len(rest) >= 3 { + topic := canonicalHelpTopic(strings.Join(rest[:2], " ")) + if _, ok := commandUsage[topic]; ok { + return strings.Fields(topic), true } - return false } - return len(args) == 2 && args[0] == "analytics" && (args[1] == "quiet" || args[1] == "trends") + topic := canonicalHelpTopic(rest[0]) + if _, ok := commandUsage[topic]; !ok { + return nil, false + } + return []string{topic}, true } -func commandHelpTopic(rest []string) []string { - if len(rest) >= 2 && hasHelpTopic(rest[:2]) { - return rest[:2] +func canonicalHelpTopic(topic string) string { + if topic == "tap" || topic == "cache-import" { + return "wiretap" + } + if topic == "attachments fetch" { + return "attachments" } - return rest[:1] + return topic } func rootHelpRequested(args []string, valueFlags ...string) bool { diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index cbd427b..da9eb41 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -4072,6 +4072,9 @@ func TestCommandHelpDoesNotOpenConfigOrStore(t *testing.T) { {"--config", filepath.Join(t.TempDir(), "missing.toml"), "coverage", "--help"}, {"--config", filepath.Join(t.TempDir(), "missing.toml"), "wiretap", "-h"}, {"--config", filepath.Join(t.TempDir(), "missing.toml"), "wiretap", "--help"}, + {"--config", filepath.Join(t.TempDir(), "missing.toml"), "sync", "--full", "--help"}, + {"--config", filepath.Join(t.TempDir(), "missing.toml"), "search", "term", "--help"}, + {"--config", filepath.Join(t.TempDir(), "missing.toml"), "remote", "login", "--endpoint", "https://example.invalid", "--help"}, } { var stdout, stderr bytes.Buffer require.NoError(t, Run(context.Background(), args, &stdout, &stderr), "args=%v", args) @@ -4087,6 +4090,13 @@ func TestCommandHelpDoesNotOpenConfigOrStore(t *testing.T) { require.Empty(t, stderr.String(), "args=%v", args) } + var stdout, stderr bytes.Buffer + require.NoError(t, Run(context.Background(), []string{"wiretap", "--help"}, &stdout, &stderr)) + require.Contains(t, stdout.String(), "--path PATH") + require.Contains(t, stdout.String(), "--watch-every DURATION") + require.Contains(t, stdout.String(), "--stats") + require.Empty(t, stderr.String()) + err := Run(context.Background(), []string{"help", "wat"}, &bytes.Buffer{}, &bytes.Buffer{}) require.Error(t, err) require.Contains(t, err.Error(), `unknown help topic "wat"`) @@ -4102,7 +4112,7 @@ func TestNestedCommandHelp(t *testing.T) { } { var stdout, stderr bytes.Buffer require.NoError(t, Run(context.Background(), args, &stdout, &stderr), "args=%v", args) - require.Contains(t, stdout.String(), "Usage: discrawl analytics [flags]", "args=%v", args) + require.Contains(t, stdout.String(), "Usage: discrawl analytics [flags]", "args=%v", args) require.Contains(t, stdout.String(), "quiet", "args=%v", args) require.Contains(t, stdout.String(), "trends", "args=%v", args) require.Empty(t, stderr.String(), "args=%v", args) @@ -4155,6 +4165,18 @@ func TestHelpers(t *testing.T) { require.True(t, hybridSemanticUnavailable(store.ErrNoCompatibleEmbeddings)) require.True(t, hybridSemanticUnavailable(assertErr("semantic query embedding missing"))) require.False(t, hybridSemanticUnavailable(assertErr("other"))) + topic, ok := earlyCommandHelpTopic([]string{"wiretap", "--help"}) + require.True(t, ok) + require.Equal(t, []string{"wiretap"}, topic) + topic, ok = earlyCommandHelpTopic([]string{"sync", "--help"}) + require.True(t, ok) + require.Equal(t, []string{"sync"}, topic) + topic, ok = earlyCommandHelpTopic([]string{"remote", "login", "--help"}) + require.True(t, ok) + require.Equal(t, []string{"remote", "login"}, topic) + topic, ok = earlyCommandHelpTopic([]string{"remote", "login", "--endpoint", "https://example.invalid", "--help"}) + require.True(t, ok) + require.Equal(t, []string{"remote", "login"}, topic) opts, err := shareOptionsFromFlags("~/share", "git@example.com:org/archive.git", "") require.NoError(t, err) require.Equal(t, "git@example.com:org/archive.git", opts.Remote) diff --git a/internal/cli/output.go b/internal/cli/output.go index e76e10f..326524c 100644 --- a/internal/cli/output.go +++ b/internal/cli/output.go @@ -96,26 +96,121 @@ func printPlain(w io.Writer, value any) error { } func printUsage(w io.Writer) error { - return printKongUsage(w, "") + return printKongUsage(w) } func printCommandUsage(w io.Writer, args []string) error { if len(args) == 0 || len(args) > 2 { return usageErr(errors.New("usage: discrawl help [subcommand]")) } - topic := strings.Join(args, " ") + topic := canonicalHelpTopic(strings.Join(args, " ")) text, ok := commandUsage[topic] if ok { _, _ = fmt.Fprint(w, text) return nil } - if !hasHelpTopic(args) { - return usageErr(fmt.Errorf("unknown help topic %q", topic)) - } - return printKongUsage(w, topic) + return usageErr(fmt.Errorf("unknown help topic %q", topic)) } var commandUsage = map[string]string{ + "metadata": `Usage: discrawl metadata [--json] + +Print the archive control manifest. +`, + "version": "Usage: discrawl version\n\nPrint the Discrawl version.\n", + "init": `Usage: discrawl init [--guild ID] [--db PATH] [--with-embeddings] + +Discover accessible guilds and initialize configuration. +`, + "sync": `Usage: discrawl sync [--full] [--all] [--all-channels] [--since RFC3339] [--channels IDS] [--concurrency N] [--source SOURCE] [--with-embeddings] [--with-media] [--skip-members|--with-members] [--latest-only] [--guild ID|--guilds IDS] [--update MODE|--no-update] + +Sync Discord or desktop-cache data into the local archive. +`, + "tail": `Usage: discrawl tail [--repair-every DURATION] [--guild ID|--guilds IDS] + +Continuously archive new Discord messages. +`, + "wiretap": `Usage: + discrawl wiretap [flags] + +Flags: + --path PATH Discord Desktop cache path. + --max-file-bytes N Maximum cache file size to inspect. + --full-cache Scan the full cache instead of recent files only. + --dry-run Inspect cache data without writing archive rows. + --watch-every DURATION Repeat imports at this interval (minimum 1s). + --stats Include archive coverage and watch deltas. + --json Write JSON output. +`, + "tui": `Usage: discrawl tui [--channel ID] [--author ID] [--limit N] [--include-empty] [--dm] [--guild ID|--guilds IDS] [--json] + +Explore the archive in an interactive terminal UI. +`, + "digest": `Usage: discrawl digest [--since DURATION] [--guild ID] [--channel ID_OR_NAME] [--top-n N] + +Summarize recent archive activity. +`, + "analytics": `Usage: discrawl analytics [flags] + +Analyze inactive channels or week-over-week message trends. +`, + "analytics quiet": `Usage: discrawl analytics quiet [--since DURATION] [--guild ID] + +List channels with no activity in the lookback window. +`, + "analytics trends": `Usage: discrawl analytics trends [--weeks N] [--guild ID] [--channel ID_OR_NAME] + +Report week-over-week message counts per channel. +`, + "dms": `Usage: discrawl dms [--with ID_OR_NAME] [--search TEXT] [--hours N|--days N|--since RFC3339] [--before RFC3339] [--limit N|--last N|--all] [--list] [--include-empty] + +List local Discord Desktop conversations or messages. +`, + "mentions": `Usage: discrawl mentions [--channel ID_OR_NAME] [--author ID_OR_NAME] [--target ID_OR_NAME] [--type user|role] [--days N|--since RFC3339] [--before RFC3339] [--limit N] [--guild ID|--guilds IDS] + +List archived mentions matching at least one filter. +`, + "embed": `Usage: discrawl embed [--limit N] [--batch-size N] [--rebuild] + +Generate embeddings for queued archive messages. +`, + "members": `Usage: discrawl members [args] + +List, inspect, or search archived Discord members. +`, + "members list": `Usage: discrawl members list + +List archived Discord members. +`, + "members show": `Usage: discrawl members show [--messages N] ID_OR_QUERY + +Show one member profile and recent messages. +`, + "members search": `Usage: discrawl members search QUERY + +Search archived Discord members. +`, + "status": `Usage: discrawl status [--json] + +Show archive status and freshness. +`, + "report": `Usage: discrawl report [--readme PATH] + +Generate the archive activity report. +`, + "doctor": `Usage: discrawl doctor [--json] + +Check configuration, storage, credentials, and optional services. +`, + "subscribe": `Usage: discrawl subscribe [--repo PATH] [--branch NAME] [--stale-after DURATION] [--no-auto-update] [--no-import] [--force] [--with-embeddings] [--no-media] REMOTE + +Configure and optionally import a read-only snapshot subscription. +`, + "update": `Usage: discrawl update [--repo PATH] [--remote URL] [--branch NAME] [--force] [--ref REF] [--with-embeddings] [--no-media] + +Update the configured snapshot subscription. Historical --ref imports require --force. +`, + "whoami": "Usage: discrawl whoami\n\nShow the configured remote identity.\n", "failures": `Usage: discrawl failures [--all] [--source SOURCE] [--guild ID] [--channel ID] [--limit N] [--json] @@ -199,6 +294,18 @@ Flags: discrawl channels resolve [--guild ID | --guilds ID,ID] [--json] ID_OR_NAME Resolution prefers an exact channel id, then an exact name, then a unique partial name. Ambiguous names fail with candidate guild/channel ids. +`, + "channels resolve": `Usage: discrawl channels resolve [--guild ID|--guilds IDS] [--json] ID_OR_NAME + +Resolve a channel id or name with actionable ambiguity candidates. +`, + "channels list": `Usage: discrawl channels list + +List archived Discord channels. +`, + "channels show": `Usage: discrawl channels show CHANNEL_ID + +Show one archived Discord channel. `, "sql": `Usage: discrawl sql [--unsafe --confirm] @@ -218,11 +325,31 @@ Read-only SQL is allowed by default. Use "-" or no query to read SQL from stdin. discrawl remote whoami Reads the configured Cloudflare-backed remote archive without opening the local SQLite database. +`, + "remote status": `Usage: discrawl remote status + +Show the configured remote archive status. +`, + "remote archives": `Usage: discrawl remote archives + +List archives visible to the configured remote identity. +`, + "remote login": `Usage: discrawl remote login [--endpoint URL] [--github-token-env ENV] [--no-browser] [--timeout DURATION] [--poll-interval DURATION] [--json] + +Authenticate to a remote archive with GitHub device flow or an explicit token environment variable. +`, + "remote whoami": `Usage: discrawl remote whoami + +Show the configured remote identity. `, "cloud": `Usage: - discrawl cloud publish --remote URL --archive ARCHIVE [--token-env ENV] + discrawl cloud publish [--remote URL] [--archive ARCHIVE] [--token-env ENV] + +Publishes the local non-DM SQLite archive into a Cloudflare-backed remote archive, using configured remote targets when flags are omitted. +`, + "cloud publish": `Usage: discrawl cloud publish [--remote URL] [--archive ID] [--token-env ENV] [--sqlite-only] [--json] -Publishes the local non-DM SQLite archive into a Cloudflare-backed remote archive. +Publish the local non-DM archive to a Cloudflare-backed remote, using configured targets when flags are omitted. `, "publish": `Usage: discrawl publish [flags]