Mega Mind v0.5.4

cli.rs

Rust · 283 lines · 9.4 KB
//! The command line: every subcommand and its flags.

use crate::index::Filter;
use crate::{setup, store, upgrade};
use clap::{Parser, Subcommand};
use std::path::PathBuf;

// Clap strips these colour codes itself when output is piped or NO_COLOR is set.
const EXAMPLES: &str = "\x1b[1;33mExamples:\x1b[0m
  mind search \"retry webhooks with backoff\"        find how it was done before
  mind search \"deploy\" --kind runbook -p api       narrow by kind and project
  mind get <id>                                    read an entry and open its files
  mind template system > e.md                      start a new entry, then: mind record --file e.md
  mind stale                                       entries whose code changed since they were written
  mind who <project>                               where a project lives and how to reach its agent
  mind setup                                       configure: name, AI provider, agent tools, projects
  mind upgrade                                     install the latest version from your clone's source

Full guide: https://megamind.coinopolis.io/source/f/docs/GUIDE.md.html (docs/GUIDE.md in the source).";

pub fn styles() -> clap::builder::Styles {
    use clap::builder::styling::{AnsiColor, Effects};
    clap::builder::Styles::styled()
        .header(AnsiColor::Yellow.on_default() | Effects::BOLD)
        .usage(AnsiColor::Yellow.on_default() | Effects::BOLD)
        .literal(AnsiColor::Cyan.on_default() | Effects::BOLD)
        .placeholder(AnsiColor::Cyan.on_default())
}

#[derive(Parser)]
#[command(
    name = "mind",
    version,
    about = "Mega Mind: shared memory for your AI agents — how things were built, \
workflows, fixes, gotchas, decisions, runbooks. Searchable by meaning.",
    after_help = EXAMPLES,
    styles = styles()
)]
pub struct Cli {
    /// Who is writing (default: $MIND_AGENT, else the project of the current directory)
    #[arg(long = "as", global = true)]
    pub agent: Option<String>,
    #[command(subcommand)]
    pub cmd: Option<Cmd>,
}

#[derive(clap::Args, Default)]
pub struct FilterArgs {
    /// Only this project (substring of project or owner)
    #[arg(long, short)]
    pub project: Option<String>,
    /// Only this kind (system, workflow, fix, gotcha, decision, runbook, overview)
    #[arg(long, short)]
    pub kind: Option<String>,
    /// Comma-separated tags the entry must all have
    #[arg(long, short)]
    pub tags: Option<String>,
    /// Engine/stack substring, e.g. unity, next.js, spring
    #[arg(long, short)]
    pub engine: Option<String>,
    /// Genre substring, e.g. survival
    #[arg(long)]
    pub genre: Option<String>,
    /// Platform substring, e.g. quest, android, web
    #[arg(long)]
    pub platform: Option<String>,
    /// Include retired and superseded entries (hidden by default)
    #[arg(long)]
    pub all: bool,
}

impl FilterArgs {
    pub fn filter(&self) -> Filter {
        Filter {
            project: self.project.clone().unwrap_or_default(),
            kind: self.kind.clone().unwrap_or_default(),
            tags: self
                .tags
                .as_deref()
                .unwrap_or("")
                .split(',')
                .map(store::norm_tag)
                .filter(|t| !t.is_empty())
                .collect(),
            engine: self.engine.clone().unwrap_or_default(),
            genre: self.genre.clone().unwrap_or_default(),
            platform: self.platform.clone().unwrap_or_default(),
            exclude: Default::default(),
        }
    }
}

#[derive(Subcommand)]
pub enum Cmd {
    /// Search by meaning and keywords
    Search {
        /// What you're trying to do or the problem, in plain words
        query: String,
        #[command(flatten)]
        f: FilterArgs,
        /// How many results
        #[arg(long, short = 'n', default_value_t = 8)]
        limit: usize,
        /// Also search the librarian's rephrasings of the query (needs a librarian provider)
        #[arg(long)]
        deep: bool,
        /// One line per result (id, kind, project, title): scan them all, then `mind get` the ones that fit
        #[arg(long, short = 'b', conflicts_with = "json")]
        brief: bool,
        /// Machine-readable output
        #[arg(long)]
        json: bool,
    },
    /// Show an entry, the state of its refs, librarian notes and related entries
    Get {
        id: String,
        /// Print only the raw markdown (to edit and pass to `mind update`)
        #[arg(long)]
        raw: bool,
        /// Also show the librarian's free-text notes (AI output, untrusted)
        #[arg(long)]
        notes: bool,
    },
    /// Entries most similar to this one
    Related { id: String },
    /// List entries, newest first
    List {
        #[command(flatten)]
        f: FilterArgs,
        #[arg(long, short = 'n', default_value_t = 50)]
        limit: usize,
        /// Machine-readable output
        #[arg(long)]
        json: bool,
    },
    /// Record a new entry (markdown with frontmatter, from stdin or --file)
    Record {
        #[arg(long)]
        file: Option<PathBuf>,
        /// Record even though a very similar entry exists
        #[arg(long)]
        new: bool,
    },
    /// Replace an entry with new content (stdin or --file); history stays in git
    Update {
        id: String,
        #[arg(long)]
        file: Option<PathBuf>,
    },
    /// Open an entry in $EDITOR, then validate and save it
    Edit { id: String },
    /// Mark an entry's refs as checked: still accurate after the files changed
    Ack { id: String },
    /// Delete an entry (it stays in git history)
    Rm {
        id: String,
        #[arg(long)]
        reason: String,
        /// Needed for runbooks and for entries other entries link to
        #[arg(long)]
        yes: bool,
    },
    /// Retire an entry that is no longer true: it drops out of search and lists but stays readable
    Retire {
        id: String,
        /// Why it no longer applies
        #[arg(long)]
        why: String,
        /// The entry that replaces it
        #[arg(long)]
        by: Option<String>,
    },
    /// Nightly upkeep: index, stale entries, librarian reviews, broken links, duplicates, contradictions
    Maintain {
        /// At most this many librarian reviews and contradiction checks per run
        #[arg(long, default_value_t = 40)]
        limit: usize,
        /// Skip everything that needs the librarian (reviews, contradiction checks)
        #[arg(long)]
        no_librarian: bool,
    },
    /// Entries whose referenced files changed or vanished since they were written
    Stale {
        #[arg(long, short)]
        project: Option<String>,
        /// All projects, not just the current directory's
        #[arg(long)]
        all: bool,
    },
    /// Have the librarian check stale entries against the code changes (LM Studio is started and restored as needed)
    Review {
        #[arg(long, short)]
        project: Option<String>,
        #[arg(long, short = 'n', default_value_t = 20)]
        limit: usize,
        /// Re-review even if a current review exists
        #[arg(long)]
        force: bool,
    },
    /// A project: where it lives, its stack, its entries, how to reach its agent
    Who { project: String },
    /// Which projects and entries use a technology, e.g. `mind uses postgres`
    Uses { term: String },
    /// List known projects
    Projects,
    /// Manage projects
    Project {
        #[command(subcommand)]
        cmd: ProjectCmd,
    },
    /// Runbook change proposals awaiting review
    Proposals {
        #[command(subcommand)]
        cmd: Option<ProposalCmd>,
    },
    /// Print the template for a kind of entry
    Template { kind: String },
    /// Describe the kinds of entries
    Kinds,
    /// Validate every entry and ref
    Check,
    /// Recent changes to the mind
    Log {
        #[arg(short = 'n', default_value_t = 25)]
        n: usize,
    },
    /// Rebuild the search index
    Reindex,
    /// Update Mega Mind itself: fetch the source it was built from, show what's new, rebuild
    Upgrade(upgrade::UpgradeArgs),
    /// Check that everything is wired up: settings, AI providers, index, agent tools
    Doctor,
    /// Overview of what's in the mind (also what plain `mind` shows)
    Stats,
    /// What agents recorded recently, grouped by project
    Digest {
        /// How far back: 24h, 3d, 2w, or a date (YYYY-MM-DD)
        #[arg(long, default_value = "24h")]
        since: String,
    },
    /// Create the mind's data directory
    Init,
    /// One-time (re-runnable) setup: your name, AI providers, agent tools, projects
    Setup(setup::SetupArgs),
    /// Claude Code hook entry points (session-start, prompt, stop)
    #[command(hide = true)]
    Hook { name: String },
}

#[derive(Subcommand)]
pub enum ProjectCmd {
    /// Add or update a project
    Add {
        name: String,
        path: String,
        #[arg(long, default_value = "")]
        description: String,
        /// Comma-separated stack, e.g. "rust 1.90, axum, postgres"
        #[arg(long, default_value = "")]
        stack: String,
        /// Its agent's session name prefix, e.g. payments-api
        #[arg(long, default_value = "")]
        session: String,
    },
}

#[derive(Subcommand)]
pub enum ProposalCmd {
    /// Show a proposal as a diff against the current entry
    Show { id: String },
    /// Apply a proposal
    Accept {
        id: String,
        #[arg(long)]
        yes: bool,
    },
    /// Discard a proposal
    Reject {
        id: String,
        #[arg(long)]
        reason: String,
    },
}