bearcli 1.4.0__tar.gz → 1.6.0__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.

Potentially problematic release.


This version of bearcli might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bearcli
3
- Version: 1.4.0
3
+ Version: 1.6.0
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.0
17
16
  Requires-Dist: textual>=8.2.8
18
17
  Requires-Dist: typer>=0.27.0
19
18
  Requires-Python: >=3.13
@@ -88,11 +87,13 @@ bearcli stats # counts, words, top tags, notes pe
88
87
  ### Terminal UI
89
88
 
90
89
  `bearcli ui` is a full Bear client in the terminal: the note list with live
91
- filtering on the left, preview and inline markdown editor on the right.
92
- `/` search - `Enter` edit - `n`/`c` new note - `t`/`T` tag/untag (with
93
- autocompletion) - `o` open in Bear - `a` archive - `d` trash - `r` reload.
94
- Notes with detected secrets are marked 🚨; encrypted notes show 🔒 and keep
95
- their content in Bear.
90
+ filtering on the left, markdown preview and inline editor on the right.
91
+ `/` search - `enter`/`e` edit (`ctrl+s` saves, with an unsaved-changes
92
+ guard) - `n`/`c` new note - `t`/`T` tag/untag - `o` open in Bear - `a`
93
+ archive - `d` trash - `1`/`2`/`3` notes/archive/trash views - `j`/`k`
94
+ navigation - `?` shows the full key map. Notes with detected secrets get a
95
+ red title, a 🚨 badge, and the secret values highlighted in the preview and
96
+ editor; encrypted notes show 🔒 and keep their content in Bear.
96
97
 
97
98
  ### Search
98
99
 
@@ -103,8 +104,8 @@ bearcli search "quarterly planing" --fuzzy # typo-tolerant, ranked by score
103
104
 
104
105
  ### Write
105
106
 
106
- Writes go through Bear's x-callback-url API - the database itself is never
107
- 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.
108
109
 
109
110
  ```sh
110
111
  bearcli create "Meeting notes" --text "agenda..." --tag work
@@ -181,6 +182,28 @@ teaching AI agents (Claude Code, etc.) how to use `bearcli` ships in
181
182
  cp -r skills/bear-notes ~/.claude/skills/ # or a project's .claude/skills/
182
183
  ```
183
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
+
184
207
  ## Development
185
208
 
186
209
  ```sh
@@ -190,7 +213,7 @@ uv run ty check src/
190
213
  uv run python scripts/check_docs.py # docs must cover every command
191
214
  ```
192
215
 
193
- Design notes and Bear database internals: [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md).
216
+ Design notes and internals: [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md).
194
217
  Contributor/agent guidelines: [AGENTS.md](AGENTS.md).
195
218
 
196
219
  ## License
@@ -64,11 +64,13 @@ bearcli stats # counts, words, top tags, notes pe
64
64
  ### Terminal UI
65
65
 
66
66
  `bearcli ui` is a full Bear client in the terminal: the note list with live
67
- filtering on the left, preview and inline markdown editor on the right.
68
- `/` search - `Enter` edit - `n`/`c` new note - `t`/`T` tag/untag (with
69
- autocompletion) - `o` open in Bear - `a` archive - `d` trash - `r` reload.
70
- Notes with detected secrets are marked 🚨; encrypted notes show 🔒 and keep
71
- their content in Bear.
67
+ filtering on the left, markdown preview and inline editor on the right.
68
+ `/` search - `enter`/`e` edit (`ctrl+s` saves, with an unsaved-changes
69
+ guard) - `n`/`c` new note - `t`/`T` tag/untag - `o` open in Bear - `a`
70
+ archive - `d` trash - `1`/`2`/`3` notes/archive/trash views - `j`/`k`
71
+ navigation - `?` shows the full key map. Notes with detected secrets get a
72
+ red title, a 🚨 badge, and the secret values highlighted in the preview and
73
+ editor; encrypted notes show 🔒 and keep their content in Bear.
72
74
 
73
75
  ### Search
74
76
 
@@ -79,8 +81,8 @@ bearcli search "quarterly planing" --fuzzy # typo-tolerant, ranked by score
79
81
 
80
82
  ### Write
81
83
 
82
- Writes go through Bear's x-callback-url API - the database itself is never
83
- 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.
84
86
 
85
87
  ```sh
86
88
  bearcli create "Meeting notes" --text "agenda..." --tag work
@@ -157,6 +159,28 @@ teaching AI agents (Claude Code, etc.) how to use `bearcli` ships in
157
159
  cp -r skills/bear-notes ~/.claude/skills/ # or a project's .claude/skills/
158
160
  ```
159
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
+
160
184
  ## Development
161
185
 
162
186
  ```sh
@@ -166,7 +190,7 @@ uv run ty check src/
166
190
  uv run python scripts/check_docs.py # docs must cover every command
167
191
  ```
168
192
 
169
- Design notes and Bear database internals: [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md).
193
+ Design notes and internals: [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md).
170
194
  Contributor/agent guidelines: [AGENTS.md](AGENTS.md).
171
195
 
172
196
  ## License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bearcli"
3
- version = "1.4.0"
3
+ version = "1.6.0"
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.0",
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,9 +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
- db = _open_db(db_path)
105
- try:
106
- notes = db.list_notes(limit=None, tag=tag_filter, with_text=True)
107
- finally:
108
- db.close()
109
- run_ui(notes, fuzzy=fuzzy, db_path=db_path, tag_filter=tag_filter)
104
+ _open_bear(db_path).close() # fail fast with a clear message if the db is missing
105
+ run_ui(fuzzy=fuzzy, db_path=db_path, tag_filter=tag_filter)