--- 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" --- 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. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.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 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 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) ``` Task 1: Create cmd/serve.go with --telegram flag and bot+scheduler lifecycle cmd/serve.go, cmd/stubs.go, cmd/root.go 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. cd /home/salva/Documents/apikey && go build ./cmd/... cmd/serve.go compiles. `keyhunter serve --help` shows --telegram and --port flags. Stubs removed. Task 2: Create cmd/schedule.go with add/list/remove/run subcommands cmd/schedule.go, cmd/schedule_test.go - 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 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 cd /home/salva/Documents/apikey && go build -o /dev/null . && go test ./cmd/... -v -count=1 -run "Schedule" schedule add/list/remove/run subcommands work. Full binary compiles. Tests pass. - `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 - `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 After completion, create `.planning/phases/17-telegram-scheduler/17-05-SUMMARY.md`