7.7 KiB
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 <message> |
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 sendworks without a daemon: Creates a lightweight agent inline (like current TUI does), processes the message, prints the response, exits. No session persistence.flynn startreplacessrc/index.ts: The old entry point becomes thestartsubcommand.flynn tuireplacessrc/tui.ts: The old TUI entry point becomes thetuisubcommand with--fullscreenflag preserved.package.jsonbinfield:"bin": { "flynn": "dist/cli/index.js" }enablesnpx flynnand global install.- Shebang:
#!/usr/bin/env nodeat top ofsrc/cli/index.ts.
Config Loading in CLI
shared.ts exports loadConfigSafe() which:
- Checks
FLYNN_CONFIGenv var or~/.config/flynn/config.yaml - Returns
{ config, error }instead of throwing - Used by commands that need config (start, send, tui, doctor)
- 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:<job-name>.
When a job fires:
- Emits
InboundMessagethrough the channel adapter'sonMessagehandler - Channel registry routes to unified message handler ->
NativeAgent - Agent processes and responds
CronScheduler.send()receives the response- Looks up the configured output channel from the channel registry
- Forwards the response to the output channel+peer
Config Schema
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
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(): CreatesCroninstances for each enabled jobdisconnect(): 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:
peerIdis 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
interface CheckResult {
status: 'pass' | 'fail' | 'warn' | 'skip';
label: string;
detail?: string;
}
type Check = (ctx: DoctorContext) => Promise<CheckResult>;
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
- CLI surface first -- foundation for the other two
- Doctor diagnostics -- uses CLI, no daemon dependency
- 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