# 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

```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<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*
