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.
- {bearcli-1.4.0 → bearcli-1.6.0}/PKG-INFO +34 -11
- {bearcli-1.4.0 → bearcli-1.6.0}/README.md +32 -8
- {bearcli-1.4.0 → bearcli-1.6.0}/pyproject.toml +11 -3
- {bearcli-1.4.0 → bearcli-1.6.0}/src/bearcli/cli/common.py +8 -21
- {bearcli-1.4.0 → bearcli-1.6.0}/src/bearcli/cli/export.py +14 -16
- {bearcli-1.4.0 → bearcli-1.6.0}/src/bearcli/cli/misc.py +9 -13
- {bearcli-1.4.0 → bearcli-1.6.0}/src/bearcli/cli/notes.py +84 -83
- {bearcli-1.4.0 → bearcli-1.6.0}/src/bearcli/cli/tags.py +24 -26
- {bearcli-1.4.0 → bearcli-1.6.0}/src/bearcli/export.py +8 -8
- {bearcli-1.4.0 → bearcli-1.6.0}/src/bearcli/gitsync.py +3 -2
- bearcli-1.6.0/src/bearcli/py.typed +0 -0
- {bearcli-1.4.0 → bearcli-1.6.0}/src/bearcli/tui.py +293 -90
- bearcli-1.4.0/src/bearcli/actions.py +0 -71
- bearcli-1.4.0/src/bearcli/db.py +0 -319
- bearcli-1.4.0/src/bearcli/markdown.py +0 -45
- bearcli-1.4.0/src/bearcli/ops.py +0 -98
- bearcli-1.4.0/src/bearcli/search.py +0 -121
- bearcli-1.4.0/src/bearcli/secrets.py +0 -203
- {bearcli-1.4.0 → bearcli-1.6.0}/LICENSE +0 -0
- {bearcli-1.4.0 → bearcli-1.6.0}/src/bearcli/__init__.py +0 -0
- {bearcli-1.4.0 → bearcli-1.6.0}/src/bearcli/cli/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bearcli
|
|
3
|
-
Version: 1.
|
|
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:
|
|
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
|
|
92
|
-
`/` search - `
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
107
|
-
|
|
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
|
|
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
|
|
68
|
-
`/` search - `
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
83
|
-
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
16
|
-
from
|
|
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
|
-
|
|
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 =
|
|
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
|
|
61
|
+
def _open_bear(path: Path) -> Bear:
|
|
67
62
|
try:
|
|
68
|
-
return
|
|
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(
|
|
88
|
+
def _require_note(bear: Bear, note_id: str) -> Note:
|
|
94
89
|
try:
|
|
95
|
-
note =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
79
|
+
bear = _open_bear(db_path)
|
|
80
80
|
try:
|
|
81
|
-
redactions:
|
|
81
|
+
redactions: ScanReport | None = None
|
|
82
82
|
if not allow_secrets:
|
|
83
83
|
with console.status("Scanning notes for secrets…", spinner="dots"):
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
90
|
-
redactions =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
+
_open_bear,
|
|
16
16
|
app,
|
|
17
17
|
console,
|
|
18
18
|
)
|
|
19
|
-
from
|
|
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
|
-
|
|
31
|
+
bear = _open_bear(db_path)
|
|
32
32
|
try:
|
|
33
|
-
notes =
|
|
34
|
-
tag_counts =
|
|
35
|
-
attachment_count, attachment_bytes =
|
|
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
|
-
|
|
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
|
|
105
|
-
|
|
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)
|