faculties 0.21.0

An office suite for AI agents: kanban, wiki, files, messaging, and a Lissajous-backed viewer — all persisted in a TribleSpace pile.
//! Reusable capabilities for TribleSpace-backed faculties.
//!
//! All 32 ordinary faculties expose native operations with explicit `cli` and
//! `mcp` frontends. Viewer and its capture binaries share notebook composition
//! and resident PNG capture. The aggregate binary serves these adapters locally
//! over sequential MCP stdio, not a deployed remote HTTP connector.
//!
//! Domain logic, schemas, observations and receipts are shared without argv or
//! output parsing. CLI host paths, devices and `@` expansion remain distinct
//! from literal MCP values and native image/audio/resource responses. Optional
//! model/rendering capabilities are checked when invoked; discovery does not
//! initialize them.

/// Crate version + baked git hash (see `build.rs`) — lets every installed
/// binary answer the stale-binary/version-skew question via `--version`.
pub const GIT_VERSION: &str = concat!(
    env!("CARGO_PKG_VERSION"),
    " (",
    env!("FACULTIES_GIT_VERSION"),
    ")"
);

/// The directory holding the durable model piles and voice reference assets
/// (`qwen3tts.pile`, `ref_voice_v2_24k.wav`, …; the nomic models live in the working pile itself, see `nomic.rs`).
///
/// Resolution: the `FACULTIES_MODEL_DIR` environment variable overrides it;
/// otherwise it defaults to `$HOME/.cache/faculties/models` (with `HOME`
/// falling back to the current directory `.` when unset). This keeps the
/// faculties off any one machine's absolute layout — callers `join` the
/// specific filename onto it.
pub fn model_dir() -> std::path::PathBuf {
    if let Some(dir) = std::env::var_os("FACULTIES_MODEL_DIR") {
        return std::path::PathBuf::from(dir);
    }
    let home = std::env::var_os("HOME").unwrap_or_else(|| std::ffi::OsString::from("."));
    std::path::PathBuf::from(home).join(".cache/faculties/models")
}

pub mod archive;
pub mod archive_agy;
pub mod archive_bm25;
pub mod archive_chatgpt;
pub mod archive_claude_code;
pub mod archive_claude_web;
pub mod archive_codex;
pub mod archive_collection;
pub mod archive_copilot;
pub mod archive_gemini;
pub mod archive_source;
pub mod atlas;
pub mod blockdag;
pub mod body;
pub mod bootstrap;
pub mod cli;
pub mod clock;
pub mod code;
pub mod cognition;
pub mod collection_names;
pub mod comb;
pub mod compass;
pub mod config;
pub mod decide;
pub mod discord;
pub mod duplex;
pub mod files;
pub mod gauge;
pub mod habits;
pub mod headspace;
pub mod hear;
pub mod imagine;
pub mod linkedin;
pub mod mail;
pub mod mail_pop;
pub mod mcp;
pub mod memory;
pub mod memory_cover;
pub mod message;
#[cfg(any(feature = "local-embed", feature = "voice", feature = "imagine"))]
mod model_storage;
#[cfg(feature = "local-embed")]
pub mod nomic;
mod organ_client;
pub mod orient;
pub mod out;
pub mod patience;
pub mod planner;
pub mod posture;
pub mod posture_finding;
pub mod posture_policy;
pub mod reason;
pub mod relations;
pub mod schemas;
pub mod spec;
pub mod status;
pub mod storage;
pub mod teams;
pub mod tokens;
pub mod triage;
pub mod turntaking;
pub mod viewer;
pub mod voice;
pub mod web;
pub mod wiki;
pub mod wiki_additive;

#[cfg(test)]
mod attribute_id_preservation_tests;
#[cfg(test)]
pub(crate) mod test_support;

/// Resolve a free-text argument that may reference a file or stdin, so every
/// faculty that takes prose content (`memory create`, `message send`,
/// `wiki create/edit`, `compass note`, `status set`, …) shares one convention:
///
/// - `@-`         → read all of stdin
/// - `@<path>`    → read the file at `<path>`
/// - `@@<text>`   → the literal string `@<text>` (escape hatch for content that
///                  genuinely begins with `@`, e.g. a memory summary opening
///                  with an `@mention`)
/// - anything else → the literal string, unchanged
///
/// `label` names the value in error messages (e.g. `"summary"`, `"message text"`).
///
/// This is the canonical CLI resolver; CLI adapters must call it rather than
/// re-implementing the `@` prefix logic. Domain operations and MCP adapters
/// accept literal content and must not call this filesystem/stdin helper. The
/// footgun this closes: passing `@-` as a plain positional argv (with the body
/// on a heredoc) silently stores the literal string `"@-"` unless the faculty
/// actually routes the argument through here.
pub fn text_arg(raw: &str, label: &str) -> anyhow::Result<String> {
    use anyhow::Context;
    use std::io::Read;
    if let Some(rest) = raw.strip_prefix("@@") {
        return Ok(format!("@{rest}"));
    }
    if let Some(path) = raw.strip_prefix('@') {
        if path == "-" {
            let mut value = String::new();
            std::io::stdin()
                .read_to_string(&mut value)
                .with_context(|| format!("read {label} from stdin"))?;
            return Ok(value);
        }
        return std::fs::read_to_string(path).with_context(|| format!("read {label} from {path}"));
    }
    Ok(raw.to_string())
}

/// The `secrets` faculty's capability + envelope-encryption core, re-exported so
/// faculties consumers can reach it as `faculties::secrets`. It
/// lives in the standalone `faculties-secrets` crate so other consumers can use
/// the same implementation without pulling the mary/GORBIE/egui stack.
pub mod secrets;

#[cfg(feature = "widgets")]
pub mod widgets;

/// Resolve an [`triblespace::core::id::Id`] from a hex string, accepting
/// either a full 32-char id or a shorter prefix.
///
/// Fast path: a 32-char input is parsed directly without consuming
/// `candidates` — the common case when the user pasted a full id from
/// earlier output.
///
/// Prefix path: every candidate is scanned; if exactly one matches,
/// it's returned. Zero matches or multiple matches return descriptive
/// errors. Callers provide the candidate iterator — typically by
/// querying the relevant space for the entity kind they care about
/// (e.g. `find!(e: Id, pattern!(&space, [{ ?e @ metadata::tag: KIND_X }]))`).
///
/// Each faculty wraps this with a kind-specific helper that knows its
/// own `KIND_*` tags. The wrapper is what command handlers should call;
/// the goal is that every faculty command accepts prefixes uniformly.
pub fn resolve_id_prefix<I>(input: &str, candidates: I) -> anyhow::Result<triblespace::core::id::Id>
where
    I: IntoIterator<Item = triblespace::core::id::Id>,
{
    use anyhow::bail;
    use triblespace::core::id::Id;
    let trimmed = input.trim().to_ascii_lowercase();
    if trimmed.is_empty() {
        bail!("empty id");
    }
    if trimmed.len() == 32 {
        return Id::from_hex(&trimmed).ok_or_else(|| anyhow::anyhow!("invalid id '{trimmed}'"));
    }
    let mut matches: std::collections::HashSet<Id> = std::collections::HashSet::new();
    for id in candidates {
        if format!("{id:x}").starts_with(&trimmed) {
            matches.insert(id);
        }
    }
    match matches.len() {
        0 => bail!("no id starts with '{trimmed}'"),
        1 => Ok(matches.into_iter().next().unwrap()),
        n => bail!("{n} matches for prefix '{trimmed}'; provide a longer prefix"),
    }
}