bear-rs 0.2.0

Rust library for reading and writing Bear.app notes via the local SQLite database
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
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 {
    /// Increase diagnostic output (-v, -vv, -vvv).
    #[arg(short = 'v', long = "verbose", global = true, action = ArgAction::Count)]
    pub verbose: u8,
    #[command(subcommand)]
    pub command: Commands,
}

// ── Shared note-selector args ──────────────────────────────────────────────────

#[derive(Args, Debug, Default)]
pub struct NoteSelector {
    /// Note ZUNIQUEIDENTIFIER.
    #[arg(index = 1)]
    pub id: Option<String>,
    /// Identify note by title (case-insensitive).
    #[arg(long, value_name = "TITLE")]
    pub title: Option<String>,
}

// ── Output args (shared by list/show/search) ──────────────────────────────────

#[derive(Args, Debug)]
pub struct OutputArgs {
    /// Comma-separated field names. Use "all" for all fields, "all,content" to include body.
    #[arg(long, value_name = "FIELDS")]
    pub fields: Option<String>,
    /// Output format: text (default) or json.
    #[arg(long, value_name = "FORMAT", default_value = "text")]
    pub format: String,
}

// ── Top-level commands ────────────────────────────────────────────────────────

#[derive(Subcommand, Debug)]
pub enum Commands {
    /// List notes.
    #[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),

    /// Print raw note content.
    #[command(after_help = "\
Examples:
  bear cat <id>
  bear cat --title \"Mars\"
  bear cat <id> --offset 0 --limit 500
")]
    Cat(CatArgs),

    /// Show note metadata.
    #[command(after_help = "\
Examples:
  bear show <id>
  bear show --title \"Mars\" --format json --fields all
  bear show <id> --fields all,content
")]
    Show(ShowArgs),

    /// Search Bear notes.
    #[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),

    /// Search for a string within a single note.
    #[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),

    /// Create a new note.
    #[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),

    /// Add content to an existing note.
    #[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),

    /// Replace the entire content of a note.
    #[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),

    /// Find and replace text within a note.
    #[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),

    /// Open a note in Bear.app.
    #[command(after_help = "\
Examples:
  bear open <id>
  bear open --title \"Mars\" --header \"Moons\" --edit
  bear open <id> --new-window
")]
    Open(OpenArgs),

    /// Move a note to trash.
    Trash(NoteSelector),

    /// Archive a note.
    Archive(NoteSelector),

    /// Restore a note from trash or archive.
    Restore(NoteSelector),

    /// Manage tags on notes.
    Tags(TagsCommand),

    /// Manage pins on notes.
    Pin(PinCommand),

    /// Manage attachments on a note.
    Attachments(AttachmentsCommand),

    /// Start an MCP server over stdio.
    #[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,
}

// ── list ──────────────────────────────────────────────────────────────────────

#[derive(Args, Debug)]
pub struct ListArgs {
    /// Filter to notes carrying this tag (matches subtags too).
    #[arg(long, value_name = "TAG")]
    pub tag: Option<String>,
    /// Sort: comma-separated field:dir pairs. Valid fields: pinned, modified, created, title.
    #[arg(
        long,
        value_name = "FIELD:DIR",
        default_value = "pinned:desc,modified:desc"
    )]
    pub sort: String,
    /// Maximum number of notes to return.
    #[arg(short = 'n', long = "limit", value_name = "N")]
    pub limit: Option<usize>,
    /// Print only the total note count.
    #[arg(long)]
    pub count: bool,
    #[command(flatten)]
    pub output: OutputArgs,
}

// ── cat ───────────────────────────────────────────────────────────────────────

#[derive(Args, Debug)]
pub struct CatArgs {
    #[command(flatten)]
    pub selector: NoteSelector,
    /// Start byte offset.
    #[arg(long, value_name = "N")]
    pub offset: Option<usize>,
    /// Maximum number of bytes to return.
    #[arg(long, value_name = "N")]
    pub limit: Option<usize>,
}

// ── show ──────────────────────────────────────────────────────────────────────

#[derive(Args, Debug)]
pub struct ShowArgs {
    #[command(flatten)]
    pub selector: NoteSelector,
    #[command(flatten)]
    pub output: OutputArgs,
}

// ── search ────────────────────────────────────────────────────────────────────

#[derive(Args, Debug)]
pub struct SearchArgs {
    /// Bear search query. Pass via positional arg or --query.
    #[arg(index = 1, value_name = "QUERY")]
    pub query: Option<String>,
    /// Alternative flag form of the query.
    #[arg(long, value_name = "QUERY", conflicts_with = "query")]
    pub query_flag: Option<String>,
    /// Maximum number of results.
    #[arg(short = 'n', long = "limit", value_name = "N")]
    pub limit: Option<usize>,
    /// Print only the total match count.
    #[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("")
    }
}

// ── search-in ─────────────────────────────────────────────────────────────────

#[derive(Args, Debug)]
pub struct SearchInArgs {
    #[command(flatten)]
    pub selector: NoteSelector,
    /// String to search for within the note.
    #[arg(long, value_name = "STRING", required = true)]
    pub string: String,
    /// Print only the match count.
    #[arg(long)]
    pub count: bool,
    /// Output format.
    #[arg(long, value_name = "FORMAT", default_value = "text")]
    pub format: String,
}

// ── create ────────────────────────────────────────────────────────────────────

#[derive(Args, Debug)]
pub struct CreateArgs {
    /// Optional note title (Bear auto-generates the # heading from it).
    #[arg(index = 1, value_name = "TITLE")]
    pub title: Option<String>,
    /// Note body. Reads from stdin when omitted.
    #[arg(long, value_name = "TEXT")]
    pub content: Option<String>,
    /// Comma-separated tags.
    #[arg(long, value_name = "TAGS")]
    pub tags: Option<String>,
    /// Return existing note if one with the same title already exists.
    #[arg(long)]
    pub if_not_exists: bool,
    #[command(flatten)]
    pub output: OutputArgs,
}

// ── append ────────────────────────────────────────────────────────────────────

#[derive(Args, Debug)]
pub struct AppendArgs {
    #[command(flatten)]
    pub selector: NoteSelector,
    /// Content to append. Reads from stdin when omitted.
    #[arg(long, value_name = "TEXT")]
    pub content: Option<String>,
    /// Where to insert: beginning or end (default: end).
    #[arg(long, value_name = "POSITION", default_value = "end")]
    pub position: String,
    /// Preserve the note's modification date.
    #[arg(long = "no-update-modified")]
    pub no_update_modified: bool,
}

// ── write ─────────────────────────────────────────────────────────────────────

#[derive(Args, Debug)]
pub struct WriteArgs {
    #[command(flatten)]
    pub selector: NoteSelector,
    /// Replacement content. Reads from stdin when omitted.
    #[arg(long, value_name = "TEXT")]
    pub content: Option<String>,
    /// Reject write if the note's current hash does not match this value.
    #[arg(long, value_name = "HASH")]
    pub base: Option<String>,
}

// ── edit ──────────────────────────────────────────────────────────────────────

#[derive(Args, Debug)]
pub struct EditArgs {
    #[command(flatten)]
    pub selector: NoteSelector,
    /// Exact text to find. Repeat for batch edits.
    #[arg(long, value_name = "TEXT", required = true, action = ArgAction::Append)]
    pub at: Vec<String>,
    /// Replacement text. Repeat for batch edits.
    #[arg(long, value_name = "TEXT", action = ArgAction::Append)]
    pub replace: Vec<String>,
    /// Text to insert after the match. Repeat for batch edits.
    #[arg(long, value_name = "TEXT", action = ArgAction::Append, conflicts_with = "replace")]
    pub insert: Vec<String>,
    /// Apply to all occurrences.
    #[arg(long)]
    pub all: bool,
    /// Case-insensitive matching.
    #[arg(long = "ignore-case")]
    pub ignore_case: bool,
    /// Match whole words only.
    #[arg(long)]
    pub word: bool,
}

// ── open ──────────────────────────────────────────────────────────────────────

#[derive(Args, Debug)]
pub struct OpenArgs {
    #[command(flatten)]
    pub selector: NoteSelector,
    /// Scroll to this heading.
    #[arg(long, value_name = "HEADING")]
    pub header: Option<String>,
    /// Drop the cursor into the editor.
    #[arg(long)]
    pub edit: bool,
    /// Open in a new window.
    #[arg(long = "new-window")]
    pub new_window: bool,
    /// Open in a floating panel that stays on top.
    #[arg(long)]
    pub float: bool,
}

// ── tags ──────────────────────────────────────────────────────────────────────

#[derive(Args, Debug)]
pub struct TagsCommand {
    #[command(subcommand)]
    pub subcommand: TagsSubcommand,
}

#[derive(Subcommand, Debug)]
pub enum TagsSubcommand {
    /// List tags (all or for a specific note).
    #[command(after_help = "\
Examples:
  bear tags list
  bear tags list <id>
  bear tags list --title \"Mars\" --format json
  bear tags list --count
")]
    List(TagsListArgs),

    /// Add tags to a note.
    #[command(after_help = "\
Examples:
  bear tags add <id> work \"work/meetings\"
  bear tags add --title \"Mars\" favorite
")]
    Add(TagsAddArgs),

    /// Remove tags from a note.
    #[command(after_help = "\
Examples:
  bear tags remove <id> draft wip
  bear tags remove --title \"Mars\" draft
")]
    Remove(TagsRemoveArgs),

    /// Rename a tag across all notes.
    #[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),

    /// Delete a tag and remove it from all notes.
    #[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,
    /// Print only the tag count.
    #[arg(long)]
    pub count: bool,
    /// Output format: text or json.
    #[arg(long, value_name = "FORMAT", default_value = "text")]
    pub format: String,
}

#[derive(Args, Debug)]
pub struct TagsAddArgs {
    #[command(flatten)]
    pub selector: NoteSelector,
    /// Tags to add (positional, one or more).
    #[arg(required = true)]
    pub tags: Vec<String>,
}

#[derive(Args, Debug)]
pub struct TagsRemoveArgs {
    #[command(flatten)]
    pub selector: NoteSelector,
    /// Tags to remove (positional, one or more).
    #[arg(required = true)]
    pub tags: Vec<String>,
}

#[derive(Args, Debug)]
pub struct TagsRenameArgs {
    /// Old tag name (positional or --from).
    #[arg(index = 1, value_name = "OLD")]
    pub old: Option<String>,
    /// New tag name (positional or --to).
    #[arg(index = 2, value_name = "NEW")]
    pub new: Option<String>,
    /// Old tag name (flag form).
    #[arg(long = "from", value_name = "TAG", conflicts_with = "old")]
    pub from: Option<String>,
    /// New tag name (flag form).
    #[arg(long = "to", value_name = "TAG", conflicts_with = "new")]
    pub to: Option<String>,
    /// Proceed even if the new name already exists (merge).
    #[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 {
    /// Tag to delete (positional or --name).
    #[arg(index = 1, value_name = "TAG")]
    pub tag: Option<String>,
    /// Tag to delete (flag form).
    #[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())
    }
}

// ── pin ───────────────────────────────────────────────────────────────────────

#[derive(Args, Debug)]
pub struct PinCommand {
    #[command(subcommand)]
    pub subcommand: PinSubcommand,
}

#[derive(Subcommand, Debug)]
pub enum PinSubcommand {
    /// List pin contexts.
    #[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),

    /// Add pins to a note.
    #[command(after_help = "\
Examples:
  bear pin add <id> global
  bear pin add <id> work projects
  bear pin add --title \"Mars\" global work
")]
    Add(PinAddArgs),

    /// Remove pins from a note.
    #[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,
    /// Output format: text or json.
    #[arg(long, value_name = "FORMAT", default_value = "text")]
    pub format: String,
}

#[derive(Args, Debug)]
pub struct PinAddArgs {
    #[command(flatten)]
    pub selector: NoteSelector,
    /// Pin contexts: "global" or tag names. One or more required.
    #[arg(required = true)]
    pub contexts: Vec<String>,
}

#[derive(Args, Debug)]
pub struct PinRemoveArgs {
    #[command(flatten)]
    pub selector: NoteSelector,
    /// Pin contexts to remove. One or more required.
    #[arg(required = true)]
    pub contexts: Vec<String>,
}

// ── attachments ───────────────────────────────────────────────────────────────

#[derive(Args, Debug)]
pub struct AttachmentsCommand {
    #[command(subcommand)]
    pub subcommand: AttachmentsSubcommand,
}

#[derive(Subcommand, Debug)]
pub enum AttachmentsSubcommand {
    /// List attachments on a note.
    #[command(after_help = "\
Examples:
  bear attachments list <id>
  bear attachments list --title \"Mars\" --format json
")]
    List(AttachmentsListArgs),

    /// Write attachment bytes to stdout.
    #[command(after_help = "\
Examples:
  bear attachments save <id> --filename photo.jpg > photo.jpg
")]
    Save(AttachmentsSaveArgs),

    /// Add an attachment to a note (reads from stdin).
    #[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),

    /// Delete an attachment from a note.
    #[command(after_help = "\
Examples:
  bear attachments delete <id> --filename photo.jpg
")]
    Delete(AttachmentsDeleteArgs),
}

#[derive(Args, Debug)]
pub struct AttachmentsListArgs {
    #[command(flatten)]
    pub selector: NoteSelector,
    /// Output format: text or json.
    #[arg(long, value_name = "FORMAT", default_value = "text")]
    pub format: String,
}

#[derive(Args, Debug)]
pub struct AttachmentsSaveArgs {
    #[command(flatten)]
    pub selector: NoteSelector,
    /// Filename of the attachment to save.
    #[arg(long, required = true, value_name = "FILENAME")]
    pub filename: String,
}

#[derive(Args, Debug)]
pub struct AttachmentsAddArgs {
    #[command(flatten)]
    pub selector: NoteSelector,
    /// Filename for the attachment.
    #[arg(long, required = true, value_name = "FILENAME")]
    pub filename: String,
}

#[derive(Args, Debug)]
pub struct AttachmentsDeleteArgs {
    #[command(flatten)]
    pub selector: NoteSelector,
    /// Filename of the attachment to delete.
    #[arg(long, required = true, value_name = "FILENAME")]
    pub filename: String,
}