bearcli 1.5.0__tar.gz → 1.6.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bearcli
3
- Version: 1.5.0
3
+ Version: 1.6.1
4
4
  Summary: The missing CLI for Bear notes — read, search, export, and manage your notes from the terminal
5
5
  Keywords: bear,notes,cli,markdown,macos
6
6
  Author: Michel Tricot
@@ -12,8 +12,7 @@ Classifier: Environment :: Console
12
12
  Classifier: Operating System :: MacOS
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Classifier: Topic :: Utilities
15
- Requires-Dist: detect-secrets>=1.5.0
16
- Requires-Dist: rapidfuzz>=3.14.5
15
+ Requires-Dist: bearkit==1.6.1
17
16
  Requires-Dist: textual>=8.2.8
18
17
  Requires-Dist: typer>=0.27.0
19
18
  Requires-Python: >=3.13
@@ -105,8 +104,8 @@ bearcli search "quarterly planing" --fuzzy # typo-tolerant, ranked by score
105
104
 
106
105
  ### Write
107
106
 
108
- Writes go through Bear's x-callback-url API - the database itself is never
109
- written. These launch the Bear app if needed.
107
+ Writes go through the Bear app itself (launching it if needed), and every
108
+ change is verified before the command reports success.
110
109
 
111
110
  ```sh
112
111
  bearcli create "Meeting notes" --text "agenda..." --tag work
@@ -183,6 +182,28 @@ teaching AI agents (Claude Code, etc.) how to use `bearcli` ships in
183
182
  cp -r skills/bear-notes ~/.claude/skills/ # or a project's .claude/skills/
184
183
  ```
185
184
 
185
+ ## Use as a library
186
+
187
+ The fundamentals ship as their own package on PyPI, `bearkit` - reading
188
+ notes, verified writes, search, and secret detection - with no CLI or TUI
189
+ dependencies (`pip install bearkit`):
190
+
191
+ ```python
192
+ from bearkit import Bear, BearWriteError
193
+
194
+ with Bear() as bear:
195
+ for note in bear.list_notes(tag="work", limit=10):
196
+ print(note.title, note.tags)
197
+
198
+ try:
199
+ bear.add_tag(bear.get_note("C44D09DC"), "from-python")
200
+ except BearWriteError:
201
+ print("Bear did not apply the change")
202
+ ```
203
+
204
+ Full reference: [docs/BEARKIT.md](docs/BEARKIT.md). The package ships typed
205
+ (`py.typed`).
206
+
186
207
  ## Development
187
208
 
188
209
  ```sh
@@ -192,7 +213,7 @@ uv run ty check src/
192
213
  uv run python scripts/check_docs.py # docs must cover every command
193
214
  ```
194
215
 
195
- Design notes and Bear database internals: [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md).
216
+ Design notes and internals: [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md).
196
217
  Contributor/agent guidelines: [AGENTS.md](AGENTS.md).
197
218
 
198
219
  ## License
@@ -81,8 +81,8 @@ bearcli search "quarterly planing" --fuzzy # typo-tolerant, ranked by score
81
81
 
82
82
  ### Write
83
83
 
84
- Writes go through Bear's x-callback-url API - the database itself is never
85
- written. These launch the Bear app if needed.
84
+ Writes go through the Bear app itself (launching it if needed), and every
85
+ change is verified before the command reports success.
86
86
 
87
87
  ```sh
88
88
  bearcli create "Meeting notes" --text "agenda..." --tag work
@@ -159,6 +159,28 @@ teaching AI agents (Claude Code, etc.) how to use `bearcli` ships in
159
159
  cp -r skills/bear-notes ~/.claude/skills/ # or a project's .claude/skills/
160
160
  ```
161
161
 
162
+ ## Use as a library
163
+
164
+ The fundamentals ship as their own package on PyPI, `bearkit` - reading
165
+ notes, verified writes, search, and secret detection - with no CLI or TUI
166
+ dependencies (`pip install bearkit`):
167
+
168
+ ```python
169
+ from bearkit import Bear, BearWriteError
170
+
171
+ with Bear() as bear:
172
+ for note in bear.list_notes(tag="work", limit=10):
173
+ print(note.title, note.tags)
174
+
175
+ try:
176
+ bear.add_tag(bear.get_note("C44D09DC"), "from-python")
177
+ except BearWriteError:
178
+ print("Bear did not apply the change")
179
+ ```
180
+
181
+ Full reference: [docs/BEARKIT.md](docs/BEARKIT.md). The package ships typed
182
+ (`py.typed`).
183
+
162
184
  ## Development
163
185
 
164
186
  ```sh
@@ -168,7 +190,7 @@ uv run ty check src/
168
190
  uv run python scripts/check_docs.py # docs must cover every command
169
191
  ```
170
192
 
171
- Design notes and Bear database internals: [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md).
193
+ Design notes and internals: [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md).
172
194
  Contributor/agent guidelines: [AGENTS.md](AGENTS.md).
173
195
 
174
196
  ## License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bearcli"
3
- version = "1.5.0"
3
+ version = "1.6.1"
4
4
  description = "The missing CLI for Bear notes — read, search, export, and manage your notes from the terminal"
5
5
  readme = "README.md"
6
6
  keywords = ["bear", "notes", "cli", "markdown", "macos"]
@@ -18,8 +18,7 @@ license = "MIT"
18
18
  license-files = ["LICENSE"]
19
19
  requires-python = ">=3.13"
20
20
  dependencies = [
21
- "detect-secrets>=1.5.0",
22
- "rapidfuzz>=3.14.5",
21
+ "bearkit==1.6.1",
23
22
  "textual>=8.2.8",
24
23
  "typer>=0.27.0",
25
24
  ]
@@ -32,6 +31,12 @@ Documentation = "https://github.com/michel-tricot/bearcli/blob/main/docs/IMPLEME
32
31
  [project.scripts]
33
32
  bearcli = "bearcli.cli:app"
34
33
 
34
+ [tool.uv.workspace]
35
+ members = ["packages/bearkit"]
36
+
37
+ [tool.uv.sources]
38
+ bearkit = { workspace = true }
39
+
35
40
  [build-system]
36
41
  requires = ["uv_build>=0.8.22,<0.9.0"]
37
42
  build-backend = "uv_build"
@@ -48,3 +53,6 @@ line-length = 120
48
53
 
49
54
  [tool.ruff.lint]
50
55
  select = ["E", "F", "W", "I", "UP", "B"]
56
+
57
+ [tool.ruff.lint.isort]
58
+ known-first-party = ["bearcli", "bearkit"]
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import sys
6
- from collections.abc import Callable
7
6
  from datetime import datetime
8
7
  from enum import StrEnum
9
8
  from pathlib import Path
@@ -12,8 +11,8 @@ from typing import Annotated
12
11
  import typer
13
12
  from rich.console import Console
14
13
 
15
- from bearcli import actions
16
- from bearcli.db import AmbiguousNoteId, BearDB, Note, note_metadata
14
+ from bearkit import Bear
15
+ from bearkit.db import AmbiguousNoteId, Note, NoteFilter
17
16
 
18
17
  app = typer.Typer(help="Read notes from the Bear note app.", no_args_is_help=True, add_completion=False)
19
18
 
@@ -34,15 +33,11 @@ class OutputFormat(StrEnum):
34
33
  text = "text"
35
34
 
36
35
 
37
- class OnlyFilter(StrEnum):
38
- pinned = "pinned"
39
- encrypted = "encrypted"
40
- trashed = "trashed"
41
- archived = "archived"
36
+ OnlyFilter = NoteFilter # typer choices come straight from the library enum
42
37
 
43
38
 
44
39
  def _note_to_dict(note: Note, with_text: bool = False) -> dict:
45
- data = note_metadata(note)
40
+ data = note.to_dict()
46
41
  if with_text:
47
42
  data["text"] = note.text
48
43
  data["attachments"] = [
@@ -63,9 +58,9 @@ DbPathOption = Annotated[
63
58
  ]
64
59
 
65
60
 
66
- def _open_db(path: Path) -> BearDB:
61
+ def _open_bear(path: Path) -> Bear:
67
62
  try:
68
- return BearDB(path)
63
+ return Bear(path)
69
64
  except FileNotFoundError as exc:
70
65
  console.print(f"[red]Error:[/red] {exc}")
71
66
  raise typer.Exit(1) from None
@@ -90,9 +85,9 @@ def _text_or_stdin(text: str | None) -> str | None:
90
85
  return text
91
86
 
92
87
 
93
- def _require_note(db: BearDB, note_id: str) -> Note:
88
+ def _require_note(bear: Bear, note_id: str) -> Note:
94
89
  try:
95
- note = db.get_note(note_id)
90
+ note = bear.get_note(note_id)
96
91
  except AmbiguousNoteId as exc:
97
92
  console.print(f"[red]Error:[/red] note id prefix {exc.prefix!r} matches several notes:")
98
93
  for full_id, title in exc.matches:
@@ -102,11 +97,3 @@ def _require_note(db: BearDB, note_id: str) -> Note:
102
97
  console.print(f"[red]Error:[/red] no note with id {note_id!r}")
103
98
  raise typer.Exit(1)
104
99
  return note
105
-
106
-
107
- def _verify(ok: Callable[[], bool], success: str, failure: str) -> None:
108
- if actions.wait_for(ok):
109
- console.print(success)
110
- else:
111
- console.print(f"[red]Error:[/red] {failure}")
112
- raise typer.Exit(1)
@@ -12,14 +12,14 @@ from rich.table import Table
12
12
 
13
13
  from bearcli.cli.common import (
14
14
  DbPathOption,
15
- _open_db,
15
+ _open_bear,
16
16
  app,
17
17
  console,
18
18
  )
19
- from bearcli.db import DEFAULT_DB_PATH
20
19
  from bearcli.export import export_notes
21
20
  from bearcli.gitsync import GitError, export_and_push
22
- from bearcli.secrets import SecretFinding, redaction_map, scan_notes
21
+ from bearkit.db import DEFAULT_DB_PATH
22
+ from bearkit.secrets import ScanReport, SecretFinding
23
23
 
24
24
 
25
25
  def _report_secrets(findings: list[SecretFinding]) -> None:
@@ -76,30 +76,29 @@ def export(
76
76
  if allow_secrets and redact_secrets:
77
77
  console.print("[red]Error:[/red] --allow-secrets and --redact-secrets are mutually exclusive")
78
78
  raise typer.Exit(2)
79
- db = _open_db(db_path)
79
+ bear = _open_bear(db_path)
80
80
  try:
81
- redactions: dict[str, dict[str, str]] | None = None
81
+ redactions: ScanReport | None = None
82
82
  if not allow_secrets:
83
83
  with console.status("Scanning notes for secrets…", spinner="dots"):
84
- candidates = db.list_notes(limit=None, include_archived=True, with_text=True)
85
- findings = scan_notes(candidates)
86
- if findings and not redact_secrets:
87
- _report_secrets(findings)
84
+ report = bear.scan_secrets()
85
+ if report and not redact_secrets:
86
+ _report_secrets(report.findings)
88
87
  raise typer.Exit(1)
89
- if findings:
90
- redactions = redaction_map(findings)
88
+ if report:
89
+ redactions = report
91
90
  with console.status("Exporting…", spinner="dots") as status:
92
91
  update = lambda msg: status.update(rich_escape(msg)) # noqa: E731
93
92
  if push:
94
93
  try:
95
- result, outcome = export_and_push(db, dest, sync=sync, progress=update, redactions=redactions)
94
+ result, outcome = export_and_push(bear.db, dest, sync=sync, progress=update, redactions=redactions)
96
95
  except GitError as exc:
97
96
  console.print(f"[red]Error:[/red] {exc}")
98
97
  raise typer.Exit(1) from None
99
98
  else:
100
- result = export_notes(db, dest, sync=sync, progress=update, redactions=redactions)
99
+ result = export_notes(bear.db, dest, sync=sync, progress=update, redactions=redactions)
101
100
  finally:
102
- db.close()
101
+ bear.close()
103
102
 
104
103
  parts = [f"{result.written} written"]
105
104
  if push:
@@ -113,6 +112,5 @@ def export(
113
112
  if result.index_updated:
114
113
  parts.append("index updated")
115
114
  if redactions:
116
- secrets_count = sum(len(v) for v in redactions.values())
117
- parts.append(f"{secrets_count} secret(s) redacted in {len(redactions)} note(s)")
115
+ parts.append(f"{len(redactions)} secret(s) redacted in {redactions.notes_affected()} note(s)")
118
116
  console.print(f"Exported to {dest}: " + ", ".join(parts))
@@ -12,11 +12,11 @@ from rich.table import Table
12
12
  from bearcli.cli.common import (
13
13
  DbPathOption,
14
14
  OutputFormat,
15
- _open_db,
15
+ _open_bear,
16
16
  app,
17
17
  console,
18
18
  )
19
- from bearcli.db import DEFAULT_DB_PATH
19
+ from bearkit.db import DEFAULT_DB_PATH
20
20
 
21
21
 
22
22
  @app.command()
@@ -28,13 +28,13 @@ def stats(
28
28
  db_path: DbPathOption = DEFAULT_DB_PATH,
29
29
  ) -> None:
30
30
  """Show statistics about the note library."""
31
- db = _open_db(db_path)
31
+ bear = _open_bear(db_path)
32
32
  try:
33
- notes = db.list_notes(limit=None, include_trashed=True, include_archived=True, with_text=True)
34
- tag_counts = db.list_tags()
35
- attachment_count, attachment_bytes = db.attachment_stats()
33
+ notes = bear.list_notes(limit=None, include_trashed=True, include_archived=True)
34
+ tag_counts = bear.list_tags()
35
+ attachment_count, attachment_bytes = bear.attachment_stats()
36
36
  finally:
37
- db.close()
37
+ bear.close()
38
38
 
39
39
  active = [n for n in notes if not n.trashed and not n.archived]
40
40
  by_year: dict[str, int] = {}
@@ -101,5 +101,5 @@ def ui(
101
101
  """Bear in the terminal: search, edit, create, tag, and organize notes."""
102
102
  from bearcli.tui import run_ui
103
103
 
104
- _open_db(db_path).close() # fail fast with a clear message if the db is missing
104
+ _open_bear(db_path).close() # fail fast with a clear message if the db is missing
105
105
  run_ui(fuzzy=fuzzy, db_path=db_path, tag_filter=tag_filter)