Files
flynn/docs/plans/2026-02-05-phase5-cli-cron-doctor-design.md
T
2026-02-05 22:10:41 -08:00

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 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:<job-name>.

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

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(): 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

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

  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