# Phase 5a Design: CLI Surface, Cron/Scheduling, Doctor Diagnostics ## Overview Three independent Phase 5 features that make Flynn usable as a proper CLI tool and proactive agent. CLI provides the command interface, cron makes Flynn schedule-driven, doctor validates setup. **Dependencies**: Phases 1-4 complete. 298 tests passing. Typecheck clean. --- ## 1. CLI Surface ### Architecture Single entry point at `src/cli/index.ts` replaces both `src/index.ts` and `src/tui.ts`. Uses **commander** for subcommand routing. `package.json` gets a `bin` field pointing to `dist/cli/index.js`. ### Commands | Command | Description | Notes | |---------|-------------|-------| | `flynn start` | Start daemon (foreground) | Delegates to `startDaemon()` | | `flynn tui` | Launch TUI | Moves existing `src/tui.ts` logic | | `flynn send ` | One-shot message, print response, exit | Standalone (no daemon needed) | | `flynn sessions` | List active sessions | Reads from SessionStore directly | | `flynn doctor` | Validate config + health | See Section 3 | | `flynn config` | Show resolved config (redacted secrets) | Loads and displays | | `flynn version` | Print version | From package.json | ### File Structure ``` src/cli/ ├── index.ts # Commander setup, bin entry point ├── start.ts # 'flynn start' -- daemon startup ├── tui.ts # 'flynn tui' -- moves existing TUI logic ├── send.ts # 'flynn send' -- one-shot agent invocation ├── sessions.ts # 'flynn sessions' -- list/inspect sessions ├── doctor.ts # 'flynn doctor' -- diagnostics ├── config-cmd.ts # 'flynn config' -- show resolved config └── shared.ts # Config loading, output formatting, error handling ``` ### Key Decisions - **`flynn send` works without a daemon**: Creates a lightweight agent inline (like current TUI does), processes the message, prints the response, exits. No session persistence. - **`flynn start` replaces `src/index.ts`**: The old entry point becomes the `start` subcommand. - **`flynn tui` replaces `src/tui.ts`**: The old TUI entry point becomes the `tui` subcommand with `--fullscreen` flag preserved. - **`package.json` `bin` field**: `"bin": { "flynn": "dist/cli/index.js" }` enables `npx flynn` and global install. - **Shebang**: `#!/usr/bin/env node` at top of `src/cli/index.ts`. ### Config Loading in CLI `shared.ts` exports `loadConfigSafe()` which: 1. Checks `FLYNN_CONFIG` env var or `~/.config/flynn/config.yaml` 2. Returns `{ config, error }` instead of throwing 3. Used by commands that need config (start, send, tui, doctor) 4. Commands that don't need config (version) skip it --- ## 2. Cron/Scheduling ### Architecture `CronScheduler` implements `ChannelAdapter` from `src/channels/types.ts`. It registers with the `ChannelRegistry` like any other adapter. Each cron job gets a session ID of `cron:`. When a job fires: 1. Emits `InboundMessage` through the channel adapter's `onMessage` handler 2. Channel registry routes to unified message handler -> `NativeAgent` 3. Agent processes and responds 4. `CronScheduler.send()` receives the response 5. Looks up the configured output channel from the channel registry 6. Forwards the response to the output channel+peer ### Config Schema ```typescript const cronJobSchema = z.object({ name: z.string().min(1), schedule: z.string().min(1), // Cron expression: "0 9 * * *" message: z.string().min(1), // Message to send to agent output: z.object({ channel: z.string().min(1), // e.g. "telegram" peer: z.string().min(1), // e.g. "12345" (chat ID) }), enabled: z.boolean().default(true), timezone: z.string().optional(), // default UTC }); // Top-level config addition: automation: z.object({ cron: z.array(cronJobSchema).default([]), }).default({}) ``` ### Example Config ```yaml automation: cron: - name: morning-briefing schedule: "0 9 * * *" message: "Give me a morning briefing: weather, calendar, news" output: channel: telegram peer: "12345" - name: daily-backup-check schedule: "0 22 * * *" message: "Check if today's backups completed successfully" output: channel: telegram peer: "12345" enabled: true timezone: "America/New_York" ``` ### Library **croner** -- lightweight, zero-dep cron scheduler for Node.js. ESM-native, timezone support, cron expression validation. ### File Structure ``` src/automation/ ├── cron.ts # CronScheduler (ChannelAdapter) + CronJob management ``` Types are inline (no separate types.ts needed for this scope). ### Lifecycle - `connect()`: Creates `Cron` instances for each enabled job - `disconnect()`: Stops all cron instances - Registers shutdown handler via daemon lifecycle - Channel registry manages start/stop ### Output Forwarding `CronScheduler` holds a reference to `ChannelRegistry`. When `send(peerId, message)` is called with the agent's response: - `peerId` is the cron job name - Looks up the job's output config - Gets the output channel adapter from registry - Calls `outputAdapter.send(job.output.peer, message)` - If output channel is unavailable, logs warning (don't crash) --- ## 3. Doctor Diagnostics ### Architecture `flynn doctor` validates configuration and checks system health. Runs standalone (no daemon needed). Each check is independent and produces a status line. ### Checks | # | Check | Pass criteria | Depends on | |---|-------|--------------|------------| | 1 | Config file exists | File present at expected path | Nothing | | 2 | Config parses | Valid YAML syntax | #1 | | 3 | Config validates | Zod schema passes | #2 | | 4 | Env vars resolved | No unresolved `${VAR}` refs | #2 | | 5 | Data directory writable | `~/.local/share/flynn` write test | Nothing | | 6 | Session DB accessible | SQLite open + query | #5 | | 7 | Model connectivity | Default model responds | #3 | | 8 | Telegram bot valid | `getMe` API call succeeds | #3 | | 9 | MCP servers available | Configured servers can start | #3 | | 10 | Skills loaded | All skill dirs load without error | #3 | ### Output Format ``` Flynn Doctor ============ [PASS] Config file exists (~/.config/flynn/config.yaml) [PASS] Config parses (valid YAML) [FAIL] Config validates: telegram.bot_token is required [SKIP] Env vars resolved (config invalid) [PASS] Data directory writable (~/.local/share/flynn) [PASS] Session DB accessible (sessions.db) [SKIP] Model connectivity (config invalid) [SKIP] Telegram bot valid (config invalid) [SKIP] MCP servers available (config invalid) [SKIP] Skills loaded (config invalid) Results: 3 passed, 1 failed, 6 skipped ``` ### Implementation ```typescript interface CheckResult { status: 'pass' | 'fail' | 'warn' | 'skip'; label: string; detail?: string; } type Check = (ctx: DoctorContext) => Promise; interface DoctorContext { configPath: string; config?: Config; // Set after config checks pass dataDir: string; } ``` Each check is a function. Checks that depend on config are skipped if config loading failed. Exit code 0 if all pass/warn/skip, 1 if any fail. ### Model Connectivity Check Sends a minimal `chat()` request: `{ messages: [{ role: 'user', content: 'ping' }], max_tokens: 1 }`. Success = any response without error. Timeout: 10 seconds. --- ## Implementation Order 1. **CLI surface first** -- foundation for the other two 2. **Doctor diagnostics** -- uses CLI, no daemon dependency 3. **Cron/scheduling** -- needs daemon running, builds on channel system ## New Dependencies - `commander` -- CLI framework (~50KB) - `croner` -- Cron scheduler (~15KB, zero deps) --- *Design Version: 1.0* *Created: 2026-02-05* *Features: CLI surface, cron/scheduling, doctor diagnostics*