297 lines
10 KiB
Markdown
297 lines
10 KiB
Markdown
---
|
|
phase: 17-telegram-scheduler
|
|
plan: 05
|
|
type: execute
|
|
wave: 3
|
|
depends_on: ["17-01", "17-02", "17-03", "17-04"]
|
|
files_modified:
|
|
- cmd/serve.go
|
|
- cmd/schedule.go
|
|
- cmd/stubs.go
|
|
- cmd/root.go
|
|
- cmd/serve_test.go
|
|
- cmd/schedule_test.go
|
|
autonomous: true
|
|
requirements: [SCHED-02]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "keyhunter serve --telegram starts bot + scheduler and blocks until signal"
|
|
- "keyhunter schedule add creates a persistent cron job"
|
|
- "keyhunter schedule list shows all jobs with cron, next run, last run"
|
|
- "keyhunter schedule remove deletes a job by name"
|
|
- "keyhunter schedule run triggers a job manually"
|
|
- "serve and schedule stubs are replaced with real implementations"
|
|
artifacts:
|
|
- path: "cmd/serve.go"
|
|
provides: "serve command with --telegram flag, bot+scheduler lifecycle"
|
|
exports: ["serveCmd"]
|
|
- path: "cmd/schedule.go"
|
|
provides: "schedule add/list/remove/run subcommands"
|
|
exports: ["scheduleCmd"]
|
|
key_links:
|
|
- from: "cmd/serve.go"
|
|
to: "pkg/bot"
|
|
via: "bot.New + bot.Start for Telegram mode"
|
|
pattern: "bot\\.New|bot\\.Start"
|
|
- from: "cmd/serve.go"
|
|
to: "pkg/scheduler"
|
|
via: "scheduler.New + scheduler.Start"
|
|
pattern: "scheduler\\.New|scheduler\\.Start"
|
|
- from: "cmd/schedule.go"
|
|
to: "pkg/scheduler"
|
|
via: "scheduler.AddJob/RemoveJob/ListJobs/RunJob"
|
|
pattern: "scheduler\\."
|
|
- from: "cmd/root.go"
|
|
to: "cmd/serve.go"
|
|
via: "rootCmd.AddCommand(serveCmd) replacing stub"
|
|
pattern: "AddCommand.*serveCmd"
|
|
---
|
|
|
|
<objective>
|
|
Wire pkg/bot/ and pkg/scheduler/ into the CLI. Replace serve and schedule stubs in cmd/stubs.go with full implementations in cmd/serve.go and cmd/schedule.go.
|
|
|
|
Purpose: Makes Telegram bot and scheduled scanning accessible via CLI commands (SCHED-02). This is the final integration plan.
|
|
Output: cmd/serve.go, cmd/schedule.go replacing stubs.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/phases/17-telegram-scheduler/17-CONTEXT.md
|
|
@.planning/phases/17-telegram-scheduler/17-01-SUMMARY.md
|
|
@.planning/phases/17-telegram-scheduler/17-02-SUMMARY.md
|
|
@.planning/phases/17-telegram-scheduler/17-03-SUMMARY.md
|
|
@.planning/phases/17-telegram-scheduler/17-04-SUMMARY.md
|
|
@cmd/root.go
|
|
@cmd/stubs.go
|
|
@cmd/scan.go
|
|
</context>
|
|
|
|
<interfaces>
|
|
<!-- From Plan 17-01 -->
|
|
From pkg/bot/bot.go:
|
|
```go
|
|
type Config struct {
|
|
Token string; AllowedChats []int64; DB *storage.DB
|
|
ScanEngine *engine.Engine; ReconEngine *recon.Engine
|
|
ProviderRegistry *providers.Registry; EncKey []byte
|
|
}
|
|
func New(cfg Config) (*Bot, error)
|
|
func (b *Bot) Start(ctx context.Context) error
|
|
func (b *Bot) Stop()
|
|
func (b *Bot) NotifyNewFindings(result scheduler.JobResult)
|
|
```
|
|
|
|
<!-- From Plan 17-02 -->
|
|
From pkg/scheduler/scheduler.go:
|
|
```go
|
|
type Config struct {
|
|
DB *storage.DB
|
|
ScanFunc func(ctx context.Context, scanCommand string) (int, error)
|
|
OnComplete func(result JobResult)
|
|
}
|
|
func New(cfg Config) (*Scheduler, error)
|
|
func (s *Scheduler) Start(ctx context.Context) error
|
|
func (s *Scheduler) Stop() error
|
|
func (s *Scheduler) AddJob(name, cronExpr, scanCommand string, notifyTelegram bool) error
|
|
func (s *Scheduler) RemoveJob(name string) error
|
|
func (s *Scheduler) ListJobs() ([]storage.ScheduledJob, error)
|
|
func (s *Scheduler) RunJob(ctx context.Context, name string) (JobResult, error)
|
|
```
|
|
|
|
From cmd/root.go:
|
|
```go
|
|
rootCmd.AddCommand(serveCmd) // currently from stubs.go
|
|
rootCmd.AddCommand(scheduleCmd) // currently from stubs.go
|
|
```
|
|
|
|
From cmd/scan.go (pattern to follow):
|
|
```go
|
|
dbPath := viper.GetString("database.path")
|
|
db, err := storage.Open(dbPath)
|
|
reg, err := providers.NewRegistry()
|
|
eng := engine.NewEngine(reg)
|
|
```
|
|
</interfaces>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Create cmd/serve.go with --telegram flag and bot+scheduler lifecycle</name>
|
|
<files>cmd/serve.go, cmd/stubs.go, cmd/root.go</files>
|
|
<action>
|
|
1. Create cmd/serve.go:
|
|
|
|
**serveCmd** (replaces stub in stubs.go):
|
|
```
|
|
Use: "serve"
|
|
Short: "Start the KeyHunter server (Telegram bot, scheduler, web dashboard)"
|
|
Long: "Starts the KeyHunter server. Use --telegram to enable the Telegram bot."
|
|
```
|
|
|
|
**Flags:**
|
|
- `--telegram` (bool, default false): Enable Telegram bot
|
|
- `--port` (int, default 8080): HTTP port for web dashboard (Phase 18, placeholder)
|
|
|
|
**RunE logic:**
|
|
1. Open DB (same pattern as cmd/scan.go — viper.GetString("database.path"), storage.Open)
|
|
2. Load encryption key (same loadOrCreateEncKey pattern from scan.go — extract to shared helper if not already)
|
|
3. Initialize providers.NewRegistry() and engine.NewEngine(reg)
|
|
4. Initialize recon.NewEngine() and register all sources (same as cmd/recon.go pattern)
|
|
|
|
5. Create scan function for scheduler:
|
|
```go
|
|
scanFunc := func(ctx context.Context, scanCommand string) (int, error) {
|
|
src := sources.NewFileSource(scanCommand, nil)
|
|
ch, err := eng.Scan(ctx, src, engine.ScanConfig{Workers: runtime.NumCPU()*4})
|
|
// collect findings, save to DB, return count
|
|
}
|
|
```
|
|
|
|
6. If --telegram:
|
|
- Read token from viper: `viper.GetString("telegram.token")` or env `KEYHUNTER_TELEGRAM_TOKEN`
|
|
- If empty, return error "telegram.token not configured (set in ~/.keyhunter.yaml or KEYHUNTER_TELEGRAM_TOKEN env)"
|
|
- Read allowed chats: `viper.GetIntSlice("telegram.allowed_chats")`
|
|
- Create bot: `bot.New(bot.Config{Token, AllowedChats, DB, ScanEngine, ReconEngine, ProviderRegistry, EncKey})`
|
|
- Create scheduler with OnComplete wired to bot.NotifyNewFindings:
|
|
```go
|
|
sched := scheduler.New(scheduler.Config{
|
|
DB: db,
|
|
ScanFunc: scanFunc,
|
|
OnComplete: func(r scheduler.JobResult) { tgBot.NotifyNewFindings(r) },
|
|
})
|
|
```
|
|
- Start scheduler in goroutine
|
|
- Start bot (blocks on long polling)
|
|
- On SIGINT/SIGTERM: bot.Stop(), sched.Stop(), db.Close()
|
|
|
|
7. If NOT --telegram (future web-only mode):
|
|
- Create scheduler without OnComplete (or with log-only callback)
|
|
- Start scheduler
|
|
- Print "Web dashboard not yet implemented (Phase 18). Scheduler running. Ctrl+C to stop."
|
|
- Block on signal
|
|
|
|
8. Signal handling: use `signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)` for clean shutdown.
|
|
|
|
2. Update cmd/stubs.go: Remove `serveCmd` and `scheduleCmd` variable declarations (they move to their own files).
|
|
|
|
3. Update cmd/root.go: The AddCommand calls stay the same — they just resolve to the new files instead of stubs.go. Verify no compilation conflicts.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/salva/Documents/apikey && go build ./cmd/...</automated>
|
|
</verify>
|
|
<done>cmd/serve.go compiles. `keyhunter serve --help` shows --telegram and --port flags. Stubs removed.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: Create cmd/schedule.go with add/list/remove/run subcommands</name>
|
|
<files>cmd/schedule.go, cmd/schedule_test.go</files>
|
|
<behavior>
|
|
- Test 1: schedule add with valid flags creates job in DB
|
|
- Test 2: schedule list with no jobs shows empty table
|
|
- Test 3: schedule remove of nonexistent job returns error message
|
|
</behavior>
|
|
<action>
|
|
1. Create cmd/schedule.go:
|
|
|
|
**scheduleCmd** (replaces stub):
|
|
```
|
|
Use: "schedule"
|
|
Short: "Manage scheduled recurring scans"
|
|
```
|
|
Parent command with subcommands (no RunE on parent — shows help if called alone).
|
|
|
|
**scheduleAddCmd:**
|
|
```
|
|
Use: "add"
|
|
Short: "Add a new scheduled scan"
|
|
```
|
|
Flags:
|
|
- `--name` (string, required): Job name
|
|
- `--cron` (string, required): Cron expression (e.g., "0 */6 * * *")
|
|
- `--scan` (string, required): Path to scan
|
|
- `--notify` (string, optional): Notification channel ("telegram" or empty)
|
|
|
|
RunE:
|
|
- Open DB
|
|
- Create scheduler.New with DB
|
|
- Call sched.AddJob(name, cron, scan, notify=="telegram")
|
|
- Print "Scheduled job '{name}' added. Cron: {cron}, Path: {scan}"
|
|
|
|
**scheduleListCmd:**
|
|
```
|
|
Use: "list"
|
|
Short: "List all scheduled scans"
|
|
```
|
|
RunE:
|
|
- Open DB
|
|
- List all jobs via db.ListScheduledJobs()
|
|
- Print table: Name | Cron | Path | Notify | Enabled | Last Run | Next Run
|
|
- Use lipgloss table formatting (same pattern as other list commands)
|
|
|
|
**scheduleRemoveCmd:**
|
|
```
|
|
Use: "remove [name]"
|
|
Short: "Remove a scheduled scan"
|
|
Args: cobra.ExactArgs(1)
|
|
```
|
|
RunE:
|
|
- Open DB
|
|
- Delete job by name
|
|
- If 0 rows affected: "No job named '{name}' found"
|
|
- Else: "Job '{name}' removed"
|
|
|
|
**scheduleRunCmd:**
|
|
```
|
|
Use: "run [name]"
|
|
Short: "Manually trigger a scheduled scan"
|
|
Args: cobra.ExactArgs(1)
|
|
```
|
|
RunE:
|
|
- Open DB, init engine (same as serve.go pattern)
|
|
- Create scheduler with scanFunc
|
|
- Call sched.RunJob(ctx, name)
|
|
- Print result: "Job '{name}' completed. Found {N} keys in {duration}."
|
|
|
|
Register subcommands: scheduleCmd.AddCommand(scheduleAddCmd, scheduleListCmd, scheduleRemoveCmd, scheduleRunCmd)
|
|
|
|
2. Create cmd/schedule_test.go:
|
|
- TestScheduleAdd_MissingFlags: Run command without --name, verify error about required flag
|
|
- TestScheduleList_Empty: Open :memory: DB, list, verify no rows (test output format)
|
|
- Use the cobra command testing pattern from existing cmd/*_test.go files
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/salva/Documents/apikey && go build -o /dev/null . && go test ./cmd/... -v -count=1 -run "Schedule"</automated>
|
|
</verify>
|
|
<done>schedule add/list/remove/run subcommands work. Full binary compiles. Tests pass.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `go build -o /dev/null .` — full binary compiles with no stub conflicts
|
|
- `go test ./cmd/... -v -run Schedule` passes
|
|
- `./keyhunter serve --help` shows --telegram flag
|
|
- `./keyhunter schedule --help` shows add/list/remove/run subcommands
|
|
- No "not implemented" messages from serve or schedule commands
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- `keyhunter serve --telegram` starts bot+scheduler (requires token config)
|
|
- `keyhunter schedule add --name=daily --cron="0 0 * * *" --scan=./repo` persists job
|
|
- `keyhunter schedule list` shows jobs in table format
|
|
- `keyhunter schedule remove daily` deletes job
|
|
- `keyhunter schedule run daily` triggers manual scan
|
|
- serve and schedule stubs fully replaced
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/17-telegram-scheduler/17-05-SUMMARY.md`
|
|
</output>
|