use std::net::IpAddr;
use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
use frigg::settings::{LexicalBackendMode, SemanticRuntimeProvider, WatchMode};
use frigg::storage::DEFAULT_RETAINED_MANIFEST_SNAPSHOTS;
#[derive(Debug, Parser)]
#[command(name = "frigg", version, about = "Frigg MCP server")]
pub(crate) struct Cli {
#[arg(long, global = true)]
pub(crate) quiet: bool,
#[arg(long, global = true)]
pub(crate) verbose: bool,
#[arg(long = "workspace-root", value_name = "PATH", global = true)]
pub(crate) workspace_roots: Vec<PathBuf>,
#[arg(
long = "max-file-bytes",
value_name = "BYTES",
env = "FRIGG_MAX_FILE_BYTES",
hide_env = true,
global = true
)]
pub(crate) max_file_bytes: Option<usize>,
#[arg(
long,
env = "FRIGG_FULL_SCIP_INGEST",
hide_env = true,
global = true,
default_value_t = true
)]
pub(crate) full_scip_ingest: bool,
#[arg(long, value_name = "PORT", global = true)]
pub(crate) mcp_http_port: Option<u16>,
#[arg(long, value_name = "HOST", global = true)]
pub(crate) mcp_http_host: Option<IpAddr>,
#[arg(long, global = true)]
pub(crate) allow_remote_http: bool,
#[arg(
long,
value_name = "TOKEN",
env = "FRIGG_MCP_HTTP_AUTH_TOKEN",
hide_env_values = true,
global = true
)]
pub(crate) mcp_http_auth_token: Option<String>,
#[arg(
long,
value_name = "BOOL",
env = "FRIGG_SEMANTIC_RUNTIME_ENABLED",
hide_env = true,
global = true
)]
pub(crate) semantic_runtime_enabled: Option<bool>,
#[arg(
long,
value_name = "PROVIDER",
env = "FRIGG_SEMANTIC_RUNTIME_PROVIDER",
hide_env = true,
global = true
)]
pub(crate) semantic_runtime_provider: Option<SemanticRuntimeProvider>,
#[arg(
long,
value_name = "MODEL",
env = "FRIGG_SEMANTIC_RUNTIME_MODEL",
hide_env = true,
global = true
)]
pub(crate) semantic_runtime_model: Option<String>,
#[arg(
long,
value_name = "BOOL",
env = "FRIGG_SEMANTIC_RUNTIME_STRICT_MODE",
hide_env = true,
global = true
)]
pub(crate) semantic_runtime_strict_mode: Option<bool>,
#[arg(
long,
value_name = "URL",
env = "FRIGG_SEMANTIC_RUNTIME_OPENAI_COMPAT_ENDPOINT",
hide_env = true,
global = true
)]
pub(crate) semantic_runtime_openai_compat_endpoint: Option<String>,
#[arg(
long,
value_name = "MODE",
env = "FRIGG_WATCH_MODE",
hide_env = true,
global = true
)]
pub(crate) watch_mode: Option<WatchMode>,
#[arg(
long,
value_name = "MODE",
env = "FRIGG_LEXICAL_BACKEND",
hide_env = true,
global = true
)]
pub(crate) lexical_backend: Option<LexicalBackendMode>,
#[arg(
long,
value_name = "PATH",
env = "FRIGG_RIPGREP_EXECUTABLE",
hide_env = true,
global = true
)]
pub(crate) ripgrep_executable: Option<PathBuf>,
#[arg(
long,
value_name = "MILLISECONDS",
env = "FRIGG_WATCH_DEBOUNCE_MS",
hide_env = true,
global = true
)]
pub(crate) watch_debounce_ms: Option<u64>,
#[arg(
long,
value_name = "MILLISECONDS",
env = "FRIGG_WATCH_RETRY_MS",
hide_env = true,
global = true
)]
pub(crate) watch_retry_ms: Option<u64>,
#[arg(
long,
value_name = "COUNT",
env = "FRIGG_WATCH_MANIFEST_FAST_CONCURRENCY",
hide_env = true,
global = true
)]
pub(crate) watch_manifest_fast_concurrency: Option<usize>,
#[arg(
long,
value_name = "COUNT",
env = "FRIGG_WATCH_SEMANTIC_FOLLOWUP_CONCURRENCY",
hide_env = true,
global = true
)]
pub(crate) watch_semantic_followup_concurrency: Option<usize>,
#[command(subcommand)]
pub(crate) command: Option<Command>,
}
#[derive(Debug, Parser)]
#[command(name = "frigg", version, about = "Frigg MCP server")]
pub(crate) struct HiddenHookCli {
#[command(subcommand)]
pub(crate) command: HiddenHookCommand,
}
#[derive(Debug, Clone, Subcommand)]
pub(crate) enum HiddenHookCommand {
#[command(hide = true)]
Hook {
#[command(subcommand)]
event: HookEvent,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Subcommand)]
pub(crate) enum HookEvent {
#[command(name = "pretooluse")]
Pretooluse,
}
#[derive(Debug, Clone, Subcommand)]
pub(crate) enum Command {
Serve,
Adopt {
#[arg(
long,
value_enum,
alias = "hook",
num_args = 0..=1,
default_missing_value = "hook"
)]
target: Vec<AdoptTarget>,
#[arg(long, default_value_t = false)]
all: bool,
#[arg(long = "policy", value_enum, default_value_t = AdoptAgentsPolicy::Lightweight)]
policy: AdoptAgentsPolicy,
#[arg(long = "skill-provider", value_enum)]
skill_provider: Vec<SkillProvider>,
#[arg(long, default_value_t = false)]
uninstall: bool,
#[arg(long, default_value_t = false)]
check: bool,
#[arg(long = "dry-run", default_value_t = false)]
dry_run: bool,
#[arg(long, default_value_t = false)]
force: bool,
},
Init,
#[command(alias = "reindex")]
Index {
#[arg(long, default_value_t = false)]
changed: bool,
},
#[command(hide = true)]
RepairStorage,
Hash,
#[command(hide = true)]
PruneStorage {
#[arg(
long = "keep-manifest-snapshots",
default_value_t = DEFAULT_RETAINED_MANIFEST_SNAPSHOTS
)]
keep_manifest_snapshots: usize,
},
#[command(visible_alias = "content")]
Context {
#[arg(long, value_name = "DATE_OR_RFC3339")]
since: Option<String>,
#[arg(long, value_name = "DATE_OR_RFC3339")]
until: Option<String>,
#[arg(long, default_value_t = false)]
json: bool,
},
Stats {
#[arg(long, default_value_t = false)]
json: bool,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
pub(crate) enum AdoptAgentsPolicy {
#[default]
Lightweight,
Expanded,
}
impl From<AdoptAgentsPolicy> for frigg::agent_directive::AgentsPolicy {
fn from(value: AdoptAgentsPolicy) -> Self {
match value {
AdoptAgentsPolicy::Lightweight => Self::Lightweight,
AdoptAgentsPolicy::Expanded => Self::Expanded,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum AdoptTarget {
ClaudeMd,
AgentsMd,
Copilot,
Cursor,
McpProject,
McpCursor,
Hook,
}
impl AdoptTarget {
pub(crate) fn path(self) -> &'static str {
match self {
Self::ClaudeMd => "CLAUDE.md",
Self::AgentsMd => "AGENTS.md",
Self::Copilot => ".github/copilot-instructions.md",
Self::Cursor => ".cursor/rules/frigg.mdc",
Self::McpProject => ".mcp.json",
Self::McpCursor => ".cursor/mcp.json",
Self::Hook => ".claude/settings.json",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum SkillProvider {
Claude,
Codex,
Cursor,
Copilot,
}
#[cfg(test)]
mod tests {
#![allow(clippy::panic)]
use clap::Parser;
use super::{
AdoptAgentsPolicy, AdoptTarget, Cli, Command, HiddenHookCli, HiddenHookCommand, HookEvent,
SkillProvider,
};
#[test]
fn hash_command_parses_without_workspace_root() {
let cli = Cli::try_parse_from(["frigg", "hash"]).expect("hash command should parse");
assert!(cli.workspace_roots.is_empty());
assert!(matches!(cli.command, Some(Command::Hash)));
}
#[test]
fn adopt_command_parses_non_hook_flags() {
let cli = Cli::try_parse_from([
"frigg",
"adopt",
"--target",
"agents-md",
"--target",
"mcp-project",
"--dry-run",
"--check",
"--force",
"--uninstall",
"--hook",
])
.expect("adopt command should parse");
match cli.command {
Some(Command::Adopt {
target,
all,
policy,
skill_provider,
uninstall,
check,
dry_run,
force,
}) => {
assert_eq!(
target,
vec![
AdoptTarget::AgentsMd,
AdoptTarget::McpProject,
AdoptTarget::Hook
]
);
assert!(!all);
assert_eq!(policy, AdoptAgentsPolicy::Lightweight);
assert!(skill_provider.is_empty());
assert!(uninstall);
assert!(check);
assert!(dry_run);
assert!(force);
}
other => panic!("expected adopt command, got {other:?}"),
}
}
#[test]
fn adopt_command_parses_skill_providers() {
let cli = Cli::try_parse_from([
"frigg",
"adopt",
"--skill-provider",
"claude",
"--skill-provider",
"copilot",
])
.expect("adopt skill-provider should parse");
match cli.command {
Some(Command::Adopt { skill_provider, .. }) => {
assert_eq!(
skill_provider,
vec![SkillProvider::Claude, SkillProvider::Copilot]
);
}
other => panic!("expected adopt command, got {other:?}"),
}
}
#[test]
fn adopt_command_parses_expanded_policy() {
let cli = Cli::try_parse_from(["frigg", "adopt", "--policy", "expanded"])
.expect("adopt command should parse expanded policy");
match cli.command {
Some(Command::Adopt {
policy: AdoptAgentsPolicy::Expanded,
..
}) => {}
other => panic!("expected expanded adopt policy, got {other:?}"),
}
}
#[test]
fn hidden_hook_pretooluse_command_parses() {
let cli = HiddenHookCli::try_parse_from(["frigg", "hook", "pretooluse"])
.expect("hidden hook command should parse");
match cli.command {
HiddenHookCommand::Hook { event } => assert_eq!(event, HookEvent::Pretooluse),
}
}
}