use clap::{ArgAction, Args, Parser, Subcommand};
#[derive(Parser, Debug)]
#[command(
name = "bear",
about = "Command line tool for reading and writing Bear notes.",
after_help = "\
Exit codes: 0 success, 1 business error, 64 usage error.
Selection:
Most commands accept a positional <id> (ZUNIQUEIDENTIFIER) or --title.
--title does a case-insensitive exact match; most-recently-modified wins
when multiple notes share the title.
Examples:
bear list
bear show --title \"Scratch\"
bear create \"Quick Note\" --content \"Body\" --tags work
bear append --title \"Scratch\" --content \"New paragraph\"
bear search \"@today @todo\"
bear tags list
bear mcp-server
"
)]
pub struct Cli {
#[arg(short = 'v', long = "verbose", global = true, action = ArgAction::Count)]
pub verbose: u8,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Args, Debug, Default)]
pub struct NoteSelector {
#[arg(index = 1)]
pub id: Option<String>,
#[arg(long, value_name = "TITLE")]
pub title: Option<String>,
}
#[derive(Args, Debug)]
pub struct OutputArgs {
#[arg(long, value_name = "FIELDS")]
pub fields: Option<String>,
#[arg(long, value_name = "FORMAT", default_value = "text")]
pub format: String,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
#[command(after_help = "\
Default fields: id, title, tags
All fields: id, title, tags, hash, length, created, modified, pins,
location, todos, done, attachments, content
Content is excluded from \"all\". Use --fields all,content to include it.
Examples:
bear list
bear list --tag work
bear list --tag work --sort modified:asc --fields id,title,modified
bear list -n 20 --format json --fields all
bear list --count
")]
List(ListArgs),
#[command(after_help = "\
Examples:
bear cat <id>
bear cat --title \"Mars\"
bear cat <id> --offset 0 --limit 500
")]
Cat(CatArgs),
#[command(after_help = "\
Examples:
bear show <id>
bear show --title \"Mars\" --format json --fields all
bear show <id> --fields all,content
")]
Show(ShowArgs),
#[command(after_help = "\
Bear search syntax: text, \"phrases\", -negation, #tag, @today, @yesterday,
@lastXdays, @date(YYYY-MM-DD), @todo, @done, @tagged, @untagged, @pinned,
@images, @files, @code, @locked, @title, @untitled, @empty.
Full reference: https://bear.app/faq/how-to-search-notes-in-bear/
Examples:
bear search \"meeting notes\"
bear search \"@today @todo\" --format json
bear search --query \"- [ ]\" --fields id,title,matches
bear search \"@todo\" --count
")]
Search(SearchArgs),
#[command(after_help = "\
Examples:
bear search-in <id> --string \"TODO\"
bear search-in --title \"Mars\" --string \"water\" --format json
bear search-in <id> --string \"TODO\" --count
")]
SearchIn(SearchInArgs),
#[command(after_help = "\
Examples:
bear create \"My Note\" --content \"Body text\"
bear create --content \"# Quick Capture\\nSome thoughts\"
bear create \"My Note\" --tags \"work,draft\" --format json
printf \"line1\\nline2\" | bear create \"My Note\" --fields id,hash
")]
Create(CreateArgs),
#[command(after_help = "\
Examples:
bear append <id> --content \"New paragraph\"
printf \"New content\" | bear append <id>
bear append --title \"Mars\" --content \"Update\" --position beginning
")]
Append(AppendArgs),
#[command(after_help = "\
Examples:
bear write <id> --base abc1234 --content \"# Title\\nBody\"
printf \"# Title\\nBody\" | bear write <id> --base abc1234
bear write <id> --content \"# Title\\nBody\"
")]
Write(WriteArgs),
#[command(after_help = "\
Examples:
bear edit <id> --at \"TODO\" --replace \"DONE\"
bear edit <id> --at \"## Notes\" --insert \"\\nNew line\"
bear edit <id> --at \"cat\" --replace \"dog\" --all --word
")]
Edit(EditArgs),
#[command(after_help = "\
Examples:
bear open <id>
bear open --title \"Mars\" --header \"Moons\" --edit
bear open <id> --new-window
")]
Open(OpenArgs),
Trash(NoteSelector),
Archive(NoteSelector),
Restore(NoteSelector),
Tags(TagsCommand),
Pin(PinCommand),
Attachments(AttachmentsCommand),
#[command(after_help = "\
Configure an MCP-aware client (Claude Desktop, claude.ai, an IDE) to launch
this binary with the mcp-server argument. The server speaks JSON-RPC 2.0 with
line-delimited JSON framing on stdin/stdout.
")]
McpServer,
}
#[derive(Args, Debug)]
pub struct ListArgs {
#[arg(long, value_name = "TAG")]
pub tag: Option<String>,
#[arg(
long,
value_name = "FIELD:DIR",
default_value = "pinned:desc,modified:desc"
)]
pub sort: String,
#[arg(short = 'n', long = "limit", value_name = "N")]
pub limit: Option<usize>,
#[arg(long)]
pub count: bool,
#[command(flatten)]
pub output: OutputArgs,
}
#[derive(Args, Debug)]
pub struct CatArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[arg(long, value_name = "N")]
pub offset: Option<usize>,
#[arg(long, value_name = "N")]
pub limit: Option<usize>,
}
#[derive(Args, Debug)]
pub struct ShowArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[command(flatten)]
pub output: OutputArgs,
}
#[derive(Args, Debug)]
pub struct SearchArgs {
#[arg(index = 1, value_name = "QUERY")]
pub query: Option<String>,
#[arg(long, value_name = "QUERY", conflicts_with = "query")]
pub query_flag: Option<String>,
#[arg(short = 'n', long = "limit", value_name = "N")]
pub limit: Option<usize>,
#[arg(long)]
pub count: bool,
#[command(flatten)]
pub output: OutputArgs,
}
impl SearchArgs {
pub fn effective_query(&self) -> &str {
self.query
.as_deref()
.or(self.query_flag.as_deref())
.unwrap_or("")
}
}
#[derive(Args, Debug)]
pub struct SearchInArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[arg(long, value_name = "STRING", required = true)]
pub string: String,
#[arg(long)]
pub count: bool,
#[arg(long, value_name = "FORMAT", default_value = "text")]
pub format: String,
}
#[derive(Args, Debug)]
pub struct CreateArgs {
#[arg(index = 1, value_name = "TITLE")]
pub title: Option<String>,
#[arg(long, value_name = "TEXT")]
pub content: Option<String>,
#[arg(long, value_name = "TAGS")]
pub tags: Option<String>,
#[arg(long)]
pub if_not_exists: bool,
#[command(flatten)]
pub output: OutputArgs,
}
#[derive(Args, Debug)]
pub struct AppendArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[arg(long, value_name = "TEXT")]
pub content: Option<String>,
#[arg(long, value_name = "POSITION", default_value = "end")]
pub position: String,
#[arg(long = "no-update-modified")]
pub no_update_modified: bool,
}
#[derive(Args, Debug)]
pub struct WriteArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[arg(long, value_name = "TEXT")]
pub content: Option<String>,
#[arg(long, value_name = "HASH")]
pub base: Option<String>,
}
#[derive(Args, Debug)]
pub struct EditArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[arg(long, value_name = "TEXT", required = true, action = ArgAction::Append)]
pub at: Vec<String>,
#[arg(long, value_name = "TEXT", action = ArgAction::Append)]
pub replace: Vec<String>,
#[arg(long, value_name = "TEXT", action = ArgAction::Append, conflicts_with = "replace")]
pub insert: Vec<String>,
#[arg(long)]
pub all: bool,
#[arg(long = "ignore-case")]
pub ignore_case: bool,
#[arg(long)]
pub word: bool,
}
#[derive(Args, Debug)]
pub struct OpenArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[arg(long, value_name = "HEADING")]
pub header: Option<String>,
#[arg(long)]
pub edit: bool,
#[arg(long = "new-window")]
pub new_window: bool,
#[arg(long)]
pub float: bool,
}
#[derive(Args, Debug)]
pub struct TagsCommand {
#[command(subcommand)]
pub subcommand: TagsSubcommand,
}
#[derive(Subcommand, Debug)]
pub enum TagsSubcommand {
#[command(after_help = "\
Examples:
bear tags list
bear tags list <id>
bear tags list --title \"Mars\" --format json
bear tags list --count
")]
List(TagsListArgs),
#[command(after_help = "\
Examples:
bear tags add <id> work \"work/meetings\"
bear tags add --title \"Mars\" favorite
")]
Add(TagsAddArgs),
#[command(after_help = "\
Examples:
bear tags remove <id> draft wip
bear tags remove --title \"Mars\" draft
")]
Remove(TagsRemoveArgs),
#[command(after_help = "\
Examples:
bear tags rename work job
bear tags rename --from draft --to published
bear tags rename old-tag existing-tag --force
")]
Rename(TagsRenameArgs),
#[command(after_help = "\
Examples:
bear tags delete draft
bear tags delete --name \"work/old\"
")]
Delete(TagsDeleteArgs),
}
#[derive(Args, Debug)]
pub struct TagsListArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[arg(long)]
pub count: bool,
#[arg(long, value_name = "FORMAT", default_value = "text")]
pub format: String,
}
#[derive(Args, Debug)]
pub struct TagsAddArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[arg(required = true)]
pub tags: Vec<String>,
}
#[derive(Args, Debug)]
pub struct TagsRemoveArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[arg(required = true)]
pub tags: Vec<String>,
}
#[derive(Args, Debug)]
pub struct TagsRenameArgs {
#[arg(index = 1, value_name = "OLD")]
pub old: Option<String>,
#[arg(index = 2, value_name = "NEW")]
pub new: Option<String>,
#[arg(long = "from", value_name = "TAG", conflicts_with = "old")]
pub from: Option<String>,
#[arg(long = "to", value_name = "TAG", conflicts_with = "new")]
pub to: Option<String>,
#[arg(long)]
pub force: bool,
}
impl TagsRenameArgs {
pub fn old_name(&self) -> Option<&str> {
self.old.as_deref().or(self.from.as_deref())
}
pub fn new_name(&self) -> Option<&str> {
self.new.as_deref().or(self.to.as_deref())
}
}
#[derive(Args, Debug)]
pub struct TagsDeleteArgs {
#[arg(index = 1, value_name = "TAG")]
pub tag: Option<String>,
#[arg(long, value_name = "TAG", conflicts_with = "tag")]
pub name: Option<String>,
}
impl TagsDeleteArgs {
pub fn tag_name(&self) -> Option<&str> {
self.tag.as_deref().or(self.name.as_deref())
}
}
#[derive(Args, Debug)]
pub struct PinCommand {
#[command(subcommand)]
pub subcommand: PinSubcommand,
}
#[derive(Subcommand, Debug)]
pub enum PinSubcommand {
#[command(after_help = "\
Examples:
bear pin list # every pin context in use
bear pin list <id> # pins on a single note
bear pin list --title \"Mars\" --format json
")]
List(PinListArgs),
#[command(after_help = "\
Examples:
bear pin add <id> global
bear pin add <id> work projects
bear pin add --title \"Mars\" global work
")]
Add(PinAddArgs),
#[command(after_help = "\
Examples:
bear pin remove <id> global
bear pin remove <id> work
bear pin remove --title \"Mars\" global work
")]
Remove(PinRemoveArgs),
}
#[derive(Args, Debug)]
pub struct PinListArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[arg(long, value_name = "FORMAT", default_value = "text")]
pub format: String,
}
#[derive(Args, Debug)]
pub struct PinAddArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[arg(required = true)]
pub contexts: Vec<String>,
}
#[derive(Args, Debug)]
pub struct PinRemoveArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[arg(required = true)]
pub contexts: Vec<String>,
}
#[derive(Args, Debug)]
pub struct AttachmentsCommand {
#[command(subcommand)]
pub subcommand: AttachmentsSubcommand,
}
#[derive(Subcommand, Debug)]
pub enum AttachmentsSubcommand {
#[command(after_help = "\
Examples:
bear attachments list <id>
bear attachments list --title \"Mars\" --format json
")]
List(AttachmentsListArgs),
#[command(after_help = "\
Examples:
bear attachments save <id> --filename photo.jpg > photo.jpg
")]
Save(AttachmentsSaveArgs),
#[command(after_help = "\
Examples:
cat photo.jpg | bear attachments add <id> --filename photo.jpg
bear attachments add <id> --filename photo.jpg < photo.jpg
")]
Add(AttachmentsAddArgs),
#[command(after_help = "\
Examples:
bear attachments delete <id> --filename photo.jpg
")]
Delete(AttachmentsDeleteArgs),
}
#[derive(Args, Debug)]
pub struct AttachmentsListArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[arg(long, value_name = "FORMAT", default_value = "text")]
pub format: String,
}
#[derive(Args, Debug)]
pub struct AttachmentsSaveArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[arg(long, required = true, value_name = "FILENAME")]
pub filename: String,
}
#[derive(Args, Debug)]
pub struct AttachmentsAddArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[arg(long, required = true, value_name = "FILENAME")]
pub filename: String,
}
#[derive(Args, Debug)]
pub struct AttachmentsDeleteArgs {
#[command(flatten)]
pub selector: NoteSelector,
#[arg(long, required = true, value_name = "FILENAME")]
pub filename: String,
}