lucida 1.1.0

Generate images and video with Google Gemini, Veo, Runway, Kling, a local ComfyUI, FLUX, Stability AI or OpenAI — a CLI and an MCP server
//! `lucida skill` — the agent skill, printed.
//!
//! # Why the binary carries it
//!
//! The skill lives at `skills/lucida/SKILL.md` and is compiled in with
//! `include_str!`, so the file in the repository *is* the file that ships. Not a
//! copy of it, not a build step that could be forgotten — the same bytes, and a
//! missing file is a compile error rather than a stale artefact.
//!
//! That matters because of who needs it. Someone installing with the one-liner
//! gets a binary and never sees the repository, so a skill that only exists on
//! GitHub is invisible to exactly the people it was written for. Shipping it
//! inside the binary also means it updates with `lucida update`: a user cannot
//! be running one version while holding another version's skill.
//!
//! # Why it prints rather than installs
//!
//! Skill directories differ by client — `~/.claude/skills`, a project's
//! `.agents/skills`, and others — and Lucida has no way to know which one is
//! meant. Guessing would either write somewhere unwanted or invent a convention
//! that is not anyone's. Printing puts that decision where it belongs:
//!
//! ```console
//! $ lucida skill > ~/.claude/skills/lucida/SKILL.md
//! ```
//!
//! The same division as everywhere else here: `generate` prints the path it
//! wrote rather than opening the file, and `config` reports what it can see
//! rather than editing a shell profile.

/// The skill itself. `include_str!` rather than a runtime read, so this cannot
/// disagree with the repository and cannot go missing at runtime.
pub const SKILL: &str = include_str!("../skills/lucida/SKILL.md");

pub fn print() {
    print!("{SKILL}");
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Parsed by lines rather than by byte offsets into `\n`.
    ///
    /// The offset version asserted `starts_with("---\n")` and passed everywhere
    /// it was written, then failed on the Windows CI runner: git checks text
    /// files out as CRLF there, so `include_str!` embedded `---\r\n`. The same
    /// shape of mistake as reading the clock once per call — a platform
    /// assumption with no platform named in it, invisible to the machine that
    /// wrote it.
    ///
    /// `.gitattributes` now pins these files to LF, which is the real fix and
    /// keeps `lucida skill` emitting the bytes the repository holds. This test
    /// is deliberately tolerant anyway: it should be checking that a client can
    /// read the frontmatter, not which line ending arrived.
    #[test]
    fn the_skill_has_the_frontmatter_a_client_needs() {
        let mut lines = SKILL.lines();
        assert_eq!(lines.next().map(str::trim), Some("---"), "no frontmatter block");

        let front: Vec<&str> = lines.by_ref().take_while(|l| l.trim() != "---").collect();
        assert!(!front.is_empty(), "unterminated frontmatter");
        let front = front.join("\n");

        assert!(front.contains("name: lucida"), "{front}");

        // The description is what a client matches on to decide the skill is
        // relevant, so an empty one makes the file unreachable rather than
        // merely terse.
        let description = front
            .lines()
            .find_map(|l| l.strip_prefix("description: "))
            .expect("no description");
        assert!(description.len() > 40, "description too thin: {description}");
    }

    /// The property the whole file depends on, enforced rather than asserted in
    /// prose.
    ///
    /// The skill claims, in its own second paragraph, to contain no capability
    /// facts — that is what lets it stay true as providers are added and change.
    /// A provider or model-family name appearing in it is the first symptom of
    /// that claim becoming false, and it is invisible to every other test.
    ///
    /// This caught a real one: the opening line read "across five providers and
    /// video through Veo", which is both a count and a provider name, in the
    /// paragraph immediately above the claim.
    #[test]
    fn the_skill_names_no_provider_or_model_family() {
        let lower = SKILL.to_lowercase();
        for name in [
            "google", "comfyui", "bfl", "stability", "openai", "gemini", "flux", "gpt-image",
            "veo", "banana", "imagen", "black forest",
        ] {
            assert!(
                !lower.contains(name),
                "the skill names `{name}` — capabilities belong in image_providers, \
                 which is current, not here, which is a snapshot"
            );
        }
    }

    /// A count rots the same way a name does, and did: the MCP schema once said
    /// "Four providers are available" and was wrong the day a fifth landed.
    #[test]
    fn the_skill_states_no_provider_count() {
        let lower = SKILL.to_lowercase();
        for count in [
            "one provider",
            "two providers",
            "three providers",
            "four providers",
            "five providers",
            "six providers",
            "seven providers",
        ] {
            assert!(!lower.contains(count), "the skill counts providers: `{count}`");
        }
    }

    /// The same rot in a different vocabulary — and the one the two bans above
    /// could not see.
    ///
    /// v0.9.0 made one provider's mask binding, and this file went on calling
    /// every mask advisory: a capability fact, stated in the file whose entire
    /// design is to state none. The name ban missed it because the sentence
    /// named no provider, and the count ban missed it because it counted
    /// nothing. It shipped inside the binary, and an agent that believed it
    /// would re-composite a render already measured pixel-exact.
    ///
    /// These two words are the only vocabulary for which kind of mask a provider
    /// has, so their presence here means the answer has been written down
    /// somewhere it cannot be kept current. It belongs in `image_providers` and
    /// `lucida models`, both generated from `MaskSupport` itself.
    #[test]
    fn the_skill_states_no_capability_semantics() {
        let lower = SKILL.to_lowercase();
        for claim in ["advisory", "binding"] {
            assert!(
                !lower.contains(claim),
                "the skill says `{claim}` — which kind of mask a provider has is a \
                 capability fact, and it belongs in image_providers"
            );
        }
    }

    /// `lucida skill` promises the bytes the repository holds, and that promise
    /// is only true if the checkout does not rewrite them.
    ///
    /// Guards `.gitattributes`: without `eol=lf`, a Windows checkout embeds
    /// CRLF and the shipped skill differs from the repository's by a byte on
    /// every line. Nothing else would notice — the file still parses, still
    /// renders, still installs.
    #[test]
    fn the_embedded_skill_is_lf_on_every_platform() {
        assert!(
            !SKILL.contains('\r'),
            "the embedded skill contains CR — has .gitattributes lost `*.md text eol=lf`?"
        );
    }

    #[test]
    fn the_skill_points_at_the_live_answer() {
        // Saying what it does not contain is only useful alongside where to get
        // it instead.
        assert!(SKILL.contains("image_providers"));
        assert!(SKILL.contains("lucida models --provider"));
        // Video needs its own pointer, and went without one for as long as it
        // had two providers and no probe to ask: the file told agents to believe
        // the probe, and for video there was nothing to believe.
        assert!(SKILL.contains("video_providers"));
    }

    /// The skill must tell an agent to *ask* when the probe offers a real choice.
    ///
    /// Owner, 2026-08-09: several providers exist so that someone holding a
    /// subset of subscriptions can use its full width — which means the choice
    /// between them, and between a provider's own models and tiers, is routinely
    /// the user's to make rather than the agent's to assume. A skill that
    /// described the options without saying to offer them would leave an agent
    /// silently picking how much of someone's money to spend.
    #[test]
    fn the_skill_says_to_offer_the_choice_rather_than_assume_it() {
        let lower = SKILL.to_lowercase();
        assert!(
            lower.contains("ask the user") || lower.contains("ask before"),
            "the skill never tells an agent to ask which option to use"
        );
        // And the guidance has to be two-sided, or it becomes a rule to ask
        // before every render — which is how advice gets ignored wholesale.
        assert!(
            lower.contains("do **not** ask when") || lower.contains("do not ask when"),
            "the skill says when to ask but never when not to, which makes it noise"
        );
    }
}