tg-notes 0.1.0__tar.gz → 0.1.2__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.
Files changed (54) hide show
  1. {tg_notes-0.1.0 → tg_notes-0.1.2}/.claude-plugin/marketplace.json +1 -1
  2. {tg_notes-0.1.0 → tg_notes-0.1.2}/.claude-plugin/plugin.json +1 -1
  3. {tg_notes-0.1.0 → tg_notes-0.1.2}/.github/workflows/ci.yml +5 -1
  4. tg_notes-0.1.2/AGENTS.md +41 -0
  5. tg_notes-0.1.2/CHANGELOG.md +339 -0
  6. {tg_notes-0.1.0 → tg_notes-0.1.2}/CLAUDE.md +19 -0
  7. tg_notes-0.1.0/README.md → tg_notes-0.1.2/PKG-INFO +82 -0
  8. tg_notes-0.1.0/PKG-INFO → tg_notes-0.1.2/README.md +49 -24
  9. tg_notes-0.1.2/TODO.md +101 -0
  10. {tg_notes-0.1.0 → tg_notes-0.1.2}/docs/architecture.md +47 -5
  11. tg_notes-0.1.2/docs/features.md +64 -0
  12. tg_notes-0.1.2/docs/keepassxc.md +148 -0
  13. tg_notes-0.1.2/docs/sandbox-testing.md +140 -0
  14. {tg_notes-0.1.0 → tg_notes-0.1.2}/pyproject.toml +5 -1
  15. tg_notes-0.1.2/scripts/sandbox.py +315 -0
  16. {tg_notes-0.1.0 → tg_notes-0.1.2}/skills/tg-notes/SKILL.md +26 -0
  17. {tg_notes-0.1.0 → tg_notes-0.1.2}/skills/tg-notes-send/SKILL.md +4 -1
  18. tg_notes-0.1.2/tests/conftest.py +28 -0
  19. tg_notes-0.1.2/tests/test_cli.py +1048 -0
  20. tg_notes-0.1.2/tests/test_config.py +128 -0
  21. {tg_notes-0.1.0 → tg_notes-0.1.2}/tests/test_contacts.py +16 -0
  22. tg_notes-0.1.2/tests/test_live_media.py +102 -0
  23. tg_notes-0.1.2/tests/test_live_transcribe.py +45 -0
  24. tg_notes-0.1.2/tests/test_mcp.py +233 -0
  25. tg_notes-0.1.2/tests/test_media.py +503 -0
  26. {tg_notes-0.1.0 → tg_notes-0.1.2}/tests/test_notes.py +9 -3
  27. tg_notes-0.1.2/tests/test_relaunch.py +245 -0
  28. tg_notes-0.1.2/tests/test_sandbox_script.py +205 -0
  29. tg_notes-0.1.2/tests/test_secrets.py +446 -0
  30. tg_notes-0.1.2/tests/test_transcribe.py +404 -0
  31. {tg_notes-0.1.0 → tg_notes-0.1.2}/tg_notes/__init__.py +1 -1
  32. tg_notes-0.1.2/tg_notes/cli.py +886 -0
  33. {tg_notes-0.1.0 → tg_notes-0.1.2}/tg_notes/config.py +29 -1
  34. tg_notes-0.1.2/tg_notes/mcp_server.py +149 -0
  35. tg_notes-0.1.2/tg_notes/relaunch.py +85 -0
  36. tg_notes-0.1.2/tg_notes/secrets.py +347 -0
  37. {tg_notes-0.1.0 → tg_notes-0.1.2}/tg_notes/telegram.py +108 -30
  38. tg_notes-0.1.2/tg_notes/transcribe.py +278 -0
  39. tg_notes-0.1.0/AGENTS.md +0 -20
  40. tg_notes-0.1.0/CHANGELOG.md +0 -133
  41. tg_notes-0.1.0/TODO.md +0 -75
  42. tg_notes-0.1.0/docs/features.md +0 -36
  43. tg_notes-0.1.0/tests/test_cli.py +0 -533
  44. tg_notes-0.1.0/tests/test_config.py +0 -67
  45. tg_notes-0.1.0/tg_notes/cli.py +0 -391
  46. {tg_notes-0.1.0 → tg_notes-0.1.2}/.github/workflows/release.yml +0 -0
  47. {tg_notes-0.1.0 → tg_notes-0.1.2}/.gitignore +0 -0
  48. {tg_notes-0.1.0 → tg_notes-0.1.2}/LICENSE +0 -0
  49. {tg_notes-0.1.0 → tg_notes-0.1.2}/tests/__init__.py +0 -0
  50. {tg_notes-0.1.0 → tg_notes-0.1.2}/tests/test_notebooks.py +0 -0
  51. {tg_notes-0.1.0 → tg_notes-0.1.2}/tests/test_send.py +0 -0
  52. {tg_notes-0.1.0 → tg_notes-0.1.2}/tests/test_setup.py +0 -0
  53. {tg_notes-0.1.0 → tg_notes-0.1.2}/tests/test_telegram.py +0 -0
  54. {tg_notes-0.1.0 → tg_notes-0.1.2}/tg_notes/contacts.py +0 -0
@@ -9,7 +9,7 @@
9
9
  "name": "tg-notes",
10
10
  "source": "./",
11
11
  "description": "Notes → compile → publish via a private Telegram group, under your own account.",
12
- "version": "0.1.0",
12
+ "version": "0.1.2",
13
13
  "homepage": "https://github.com/korkin25/tg_notes",
14
14
  "license": "GPL-3.0-or-later",
15
15
  "keywords": ["telegram", "notes", "userbot", "reports", "cli"]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tg-notes",
3
3
  "description": "Notes → compile → publish via a private Telegram group, posting under your own account. Bundles the tg-notes capture and compile-&-send skills, which drive the tg-notes CLI (install separately: pipx install tg-notes).",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "author": {
6
6
  "name": "korkin25",
7
7
  "url": "https://github.com/korkin25"
@@ -17,7 +17,11 @@ jobs:
17
17
  with:
18
18
  python-version: ${{ matrix.python-version }}
19
19
  - name: Install (with dev extras)
20
- run: pip install -e .[dev]
20
+ # Upgrade pip/setuptools first so pip-audit does not flag the runner's own
21
+ # (incidental) build tooling — setuptools is not a runtime dependency here.
22
+ run: |
23
+ python -m pip install --upgrade pip setuptools
24
+ pip install -e .[dev]
21
25
  - name: Lint (ruff)
22
26
  run: ruff check .
23
27
  - name: Test (pytest)
@@ -0,0 +1,41 @@
1
+ # AGENTS.md
2
+
3
+ This file is the standard entry point that AI coding agents look for.
4
+
5
+ **The canonical rules for this repository live in [CLAUDE.md](CLAUDE.md).** Read it
6
+ first: it defines the language rules, the documentation-sync policy, the security
7
+ conventions, and the autonomous development workflow (TDD, feature branches,
8
+ Conventional Commits, CI, releases). Everything in `CLAUDE.md` applies to every
9
+ agent working here, not only to Claude Code.
10
+
11
+ ## Cross-agent portability
12
+
13
+ This project follows the **Agent Skills** standard (<https://agentskills.io>) and its
14
+ `SKILL.md` format. Skills are authored once as plain `SKILL.md` files (standard core
15
+ frontmatter: `name`, `description`) so the same skill is read unchanged by any
16
+ Agent-Skills-compatible runtime — Claude Code, OpenCode, Hermes, and others — with no
17
+ per-agent rewrite. Only *distribution* differs per agent; the skill content stays portable.
18
+
19
+ ### What ships
20
+
21
+ - **CLI — `tg-notes`** (on PyPI: `pipx install tg-notes`). Does all Telegram I/O; agent-
22
+ neutral. Every skill below just shells out to it, so it must be on `PATH`.
23
+ - **Two skills** (`skills/`), portable as-is:
24
+ - `tg-notes` — capture a note (`tg-notes note add`).
25
+ - `tg-notes-send` — compile stored notes per a contact's style and send them
26
+ (`tg-notes send`), with a mandatory confirmation; daily-report preset included.
27
+
28
+ ### Per-agent distribution (skill content is identical everywhere)
29
+
30
+ - **Claude Code** — a plugin from the git marketplace in this repo
31
+ (`.claude-plugin/`): `/plugin marketplace add korkin25/tg_notes` then
32
+ `/plugin install tg-notes@tg-notes-marketplace`.
33
+ - **OpenCode / OpenClaw** — discover `~/.claude/skills/*/SKILL.md` and `.claude/skills/*`
34
+ natively; drop or symlink the two `skills/` dirs there.
35
+ - **Hermes** (Nous Research) — agentskills.io-compatible but keeps skills in its own
36
+ `~/.hermes/` store; import the same `SKILL.md` files unchanged (and it can call the
37
+ `tg-notes` CLI via its terminal toolset / MCP).
38
+ - Keep the frontmatter to the standard core (`name`, `description`); avoid Claude-only
39
+ fields so the files stay runtime-agnostic.
40
+
41
+ For anything an agent needs to know or do in this repo, defer to `CLAUDE.md`.
@@ -0,0 +1,339 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
6
+ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.2] - 2026-07-25
11
+
12
+ ### Added
13
+
14
+ - Audio transcription now **auto-fetches the whisper engine on first use**. When a
15
+ transcription is needed and no engine is present, `transcribe.ensure_engine(cfg)` installs
16
+ `faster-whisper` once per process — best-effort: `pipx inject tg-notes faster-whisper`
17
+ inside a pipx-managed venv (detected via `"pipx"` in `sys.prefix` + a `pipx` on `PATH`),
18
+ otherwise `<python> -m pip install faster-whisper` — then re-checks and proceeds; the model
19
+ itself still downloads on first `WhisperModel` use. It is **best-effort and never aborts the
20
+ upload**: a failed or non-zero install logs a one-line stderr hint (`install it manually:
21
+ pipx inject tg-notes faster-whisper`) and the file uploads with no caption, exactly as when
22
+ no engine is available. A module-level guard attempts the install at most once per process.
23
+ New non-secret config key `transcriber_autoinstall` (absent/`None` ⇒ enabled; set
24
+ `transcriber_autoinstall = false` to disable — e.g. to always pre-install the engine
25
+ yourself). No CLI/MCP surface change — the auto-fetch happens transparently inside the
26
+ existing `transcribe()` path. Fully-mocked tests in `tests/test_transcribe.py` (existing
27
+ engine short-circuit, disabled, pipx-inject vs pip-install argv, non-zero/raising install,
28
+ the one-time guard) and a config round-trip in `tests/test_config.py`.
29
+ - Sandbox testing helper + mandatory-sandbox rule: new `scripts/sandbox.py` automates a
30
+ THROWAWAY, fully isolated tg-notes install for all live/integration testing so the real
31
+ `~/.config/tg-notes`, the real keyring, and the real storage group are never touched. It
32
+ seeds a sandbox config dir (`$TG_NOTES_SANDBOX_DIR` or `~/.config/tg-notes-sandbox`) from
33
+ the real credentials (real `api_id` from config, real `api_hash` + session from the keyring,
34
+ read with overrides unset), uses the **file** secrets backend so `tg-notes setup` runs
35
+ non-interactively, and creates a **dedicated test group** (a fresh `-100…` id, never the
36
+ real one). Subcommands: `setup` (idempotent seed + prints the `export TG_NOTES_CONFIG_DIR`
37
+ line), `run -- <cmd>` (exec a command against the sandbox), `pytest -- <args>` (run the
38
+ gated live tests with `TG_NOTES_LIVE=1`), and `reset` (delete the sandbox). The sandbox
39
+ session file is a full-access credential (`chmod 600`, outside the repo, never committed).
40
+ CLAUDE.md now makes sandbox isolation **mandatory** for any test touching real
41
+ config/keyring/Telegram, and `docs/sandbox-testing.md` documents the helper alongside the
42
+ existing env-var protocol.
43
+
44
+ ## [0.1.1] - 2026-07-25
45
+
46
+ ### Added
47
+
48
+ - Media notes — Phase 3 (TGN-21): media capture is now exposed on the agent surfaces. The
49
+ local MCP server gains a `note_add_file(file, notebook="daily", caption=None,
50
+ transcribe=True, hashtags=None)` tool that uploads a **local file** as a note (native
51
+ Telegram media, kind auto-detected) and returns the same dict as `telegram.note_add_file`.
52
+ It mirrors the CLI handler's best-effort audio transcription: for an audio file with no
53
+ `caption` and an available local engine it transcribes to the caption (offloaded via
54
+ `asyncio.to_thread`), and on `TranscriptionUnavailable`/`TranscriptionError` it logs to
55
+ stderr and uploads with no caption — transcription never aborts the upload; a missing file
56
+ raises a clear error before any core call. An agent may pass its own `caption` (e.g. an
57
+ image description) to skip transcription, or `transcribe=False`. The `notes_list` tool
58
+ already surfaces each note's `media` key verbatim. The `tg-notes` capture skill documents
59
+ media capture (`note add --file`, audio auto-transcription, agent-supplied captions), and
60
+ `tg-notes-send` notes that media notes compile from their caption (`media` + caption-as-`text`).
61
+ - Media notes — Phase 2 (TGN-20): audio note files **auto-transcribe to the caption**. When
62
+ `note add --file <path>` is an audio file (`.ogg`/`.oga`/`.opus`/`.mp3`/`.m4a`/`.wav`/
63
+ `.flac`/`.aac`/`.webm`) and no `--caption` is given, tg-notes transcribes it **locally**
64
+ and uses the transcript as the caption — so a dictated voice note becomes searchable text
65
+ with no manual step. New `tg_notes/transcribe.py` provides a **pluggable** local
66
+ transcriber detected on demand in order: a configured whisper CLI (`whisper_cmd`), a
67
+ whisper CLI on `PATH` (`whisper-cli` / `whisper` / `main` — whisper.cpp or openai-whisper),
68
+ then the `faster-whisper` package (imported lazily). It is **best-effort**: if no engine is
69
+ installed (`TranscriptionUnavailable`) or transcription fails (`TranscriptionError`) the
70
+ file **still uploads** without a caption and a one-line stderr hint/warning is printed —
71
+ transcription never aborts the upload. `telegram.py` stays pure Telegram I/O; the CLI
72
+ handler does the transcription and passes the text into the existing `note_add_file` as the
73
+ caption. New `--transcribe`/`--no-transcribe` flag on `note add` (default = auto: only for
74
+ an audio file with no `--caption` and an available engine); a non-audio file, an explicit
75
+ `--caption`, or `--no-transcribe` skips it. New non-secret config keys `transcriber`,
76
+ `whisper_cmd`, `whisper_model`, and a `tg-notes[transcribe]` extra
77
+ (`pipx inject tg-notes faster-whisper`). The whisper engines need `ffmpeg` to decode audio.
78
+ Fully-mocked tests in `tests/test_transcribe.py` (backends, detection order, CLI arg
79
+ templates, error paths) and `tests/test_media.py` (CLI wiring); a gated live test in
80
+ `tests/test_live_transcribe.py` (skipped unless `TG_NOTES_LIVE=1` and an engine is present).
81
+ - Media notes — Phase 1 (TGN-19): `tg-notes note add --file <path> [--caption <text>]`
82
+ uploads a photo, video, audio, or document into the notebook topic as **native Telegram
83
+ media** (Telethon auto-detects the kind), creating the topic on demand. The `--caption`
84
+ (plus any repeatable `--hashtag`) becomes the message caption — the note's searchable
85
+ text; a missing file is rejected with a clear error before any network call. New
86
+ `telegram.note_add_file` returns `{notebook, topic_id, message_id, date, media_type,
87
+ caption}` (`media_type` ∈ `photo`/`voice`/`audio`/`video`/`gif`/`document`). `notes_list`
88
+ is now additive: every note dict carries a `media` key (the type, or `null` for text) and
89
+ for a media note `text` is its caption, so text and media notes compile through the same
90
+ path. `note add`'s `--text-file` is now optional (use it or `--file`). Fully-mocked tests
91
+ in `tests/test_media.py`; a gated live round-trip in `tests/test_live_media.py` (skipped
92
+ unless `TG_NOTES_LIVE=1`). Captions are passed explicitly for now — Phase 2 will auto-fill
93
+ an audio note's caption from its transcription.
94
+ - On Linux with the keyring backend, the CLI re-execs through a named launcher
95
+ (`<venv>/libexec/tg-notes`, a copy of the interpreter with the venv's site-packages
96
+ re-added via `site.addsitedir` under `-S`) so the vault confirmation prompt
97
+ (KeePassXC / Secret Service) identifies the app as `tg-notes` instead of `python3.12`.
98
+ The launcher is created once and reused; best-effort — a read-only venv (or any
99
+ copy/exec failure) silently skips it and the CLI keeps working (the prompt then shows
100
+ `python`). `TG_NOTES_RELAUNCHED` is an internal loop guard. No-op on non-Linux, on the
101
+ file backend, or in frozen builds.
102
+ - `docs/sandbox-testing.md` — a protocol for isolated end-to-end testing of
103
+ `setup`/`secrets doctor`/`secrets migrate`/pickers via `TG_NOTES_CONFIG_DIR` +
104
+ `TG_NOTES_KEYRING_SERVICE`, so a throwaway install never touches the real
105
+ config/session/vault (with before/after invariants on the real config + `tg-notes`
106
+ keyring entries).
107
+ - `TG_NOTES_CONFIG_DIR` and `TG_NOTES_KEYRING_SERVICE` env vars for an isolated sandbox
108
+ install: `TG_NOTES_CONFIG_DIR` overrides the exact config directory (its own `config.toml`
109
+ + `*.session`, taking precedence over XDG), and `TG_NOTES_KEYRING_SERVICE` overrides the
110
+ keyring service namespace so a sandbox `secrets migrate` never overwrites the real
111
+ `tg-notes` vault entries — safe end-to-end testing of `setup`/`secrets doctor`/`migrate`.
112
+ With neither set, behavior is unchanged. `secrets doctor` and `secrets status` now show
113
+ the active config dir and keyring service.
114
+ - Interactive pickers for `send --contact`, `contacts remove`, `secrets migrate --to`, and
115
+ `notes list --notebook` — omit the value in an interactive terminal to choose from a list
116
+ via a fuzzy finder (`fzf`/`sk`/`fzy`) when one is installed, else a numbered menu. The
117
+ picker fires only when both stdin and stdout are TTYs *and* the value was not passed on the
118
+ command line, so scripted/agent use with flags is byte-for-byte unchanged; a non-interactive
119
+ run without the value fails fast with a clear message (exit 2). `notes list --notebook` still
120
+ falls back to `daily` when non-interactive or when the pick is cancelled.
121
+ - `tg-notes secrets doctor` — diagnoses the secret store (active backend, a classified
122
+ vault round-trip via `keyring_probe`, the process owning the Secret Service bus, and the
123
+ detected stores) and prints ordered, actionable recommendations: install the
124
+ `tg-notes[keyring]` extra, expose a KeePassXC group, turn off KeePassXC per-access
125
+ confirmation, hand the Secret Service bus from gnome-keyring over to KeePassXC, or migrate
126
+ when the vault is ready (`--json` for machine-readable output). `secrets migrate --to
127
+ keyring` now runs the same pre-flight and prints those recommendations instead of a
128
+ generic error when the vault isn't ready, and `setup` ends with a tip pointing at
129
+ `secrets doctor`.
130
+ - `docs/keepassxc.md` — a guide to using KeePassXC as tg-notes' secret store: handing
131
+ `org.freedesktop.secrets` over from gnome-keyring (three reversible user-level changes
132
+ + revert), exposing a dedicated minimal group, the per-connection confirmation model
133
+ (why a short-lived CLI re-prompts / locks under `ConfirmAccessItem`, keepassxc#6458),
134
+ the resulting security model, and the `secrets doctor`/`migrate` workflow.
135
+ - `tg-notes secrets status` now lists the detected secret stores (`available_stores`),
136
+ each annotated with whether it serves the Secret Service or is merely running —
137
+ gnome-keyring, KeePassXC, KWallet/ksecretd — so it's clear which vault the keyring
138
+ backend would actually use and what you could switch to.
139
+ - Pluggable secrets backend (TGN-18): `tg_notes/secrets.py` abstracts where the two real
140
+ secrets — `api_hash` and the Telethon session — live. **file** (default, unchanged): in
141
+ `config.toml` (600) + a `*.session` file. **keyring** (opt-in, `tg-notes[keyring]`): in
142
+ the OS Secret Service via the `keyring` library (gnome-keyring / KWallet / KeePassXC —
143
+ whichever owns `org.freedesktop.secrets`), the session stored as a Telethon
144
+ `StringSession`; provider-agnostic, so enabling KeePassXC's Secret Service integration
145
+ routes secrets there. `api_id`/`storage_group_id` stay in config (not secret). New CLI:
146
+ `tg-notes secrets status` (active backend, whether configured/has-session, keyring
147
+ availability, and which process owns the Secret Service bus) and `tg-notes secrets
148
+ migrate --to file|keyring` (moves both secrets; keyring migration verifies the vault
149
+ round-trip before removing the on-disk session). `build_client`/`login` in `telegram.py`
150
+ now resolve credentials + session through the active backend. Verified end-to-end on the
151
+ real account (isolated session copy: file → keyring → `whoami` from the vault → back to
152
+ file), the file default untouched. Tests in `tests/test_secrets.py`.
153
+ - Local MCP server (TGN-17): `tg-notes-mcp` — a stdio MCP server (`mcp` / FastMCP) that
154
+ exposes the core as tools `note_add` / `notes_list` / `contacts_list` / `send`, so agent
155
+ hosts that can't shell out (Claude Desktop, …) can drive tg-notes. Same local core; the
156
+ Telethon session/secrets stay on the machine (stdio only, nothing hosted). Tools are
157
+ async and offload the blocking Telethon calls to a worker thread (the sync core can't run
158
+ inside the server's event loop). Ships behind an optional extra:
159
+ `pipx install "tg-notes[mcp]"`; new `tg_notes/mcp_server.py`, tests in `tests/test_mcp.py`.
160
+ Verified end-to-end against the real account (all four tools via the server).
161
+
162
+ ### Changed
163
+
164
+ - The keyring backend now reads/writes the vault via `secretstorage` with an explicit
165
+ unlock-and-wait (plus a small re-request retry) instead of the plain `keyring` API, so a
166
+ per-access-confirmation vault (KeePassXC) makes tg-notes **wait for the confirmation
167
+ prompt** — like every other app — rather than failing outright with "locked". Locked
168
+ collections/items are `unlock()`ed (which blocks until the user answers), retrying if the
169
+ prompt is dismissed. Existing `keyring`-written entries remain compatible (looked up by
170
+ the `{service, username}` attributes); where `secretstorage` is unavailable (non-Linux)
171
+ it falls back to the plain `keyring` API. The `tg-notes[keyring]` extra now also pulls
172
+ `secretstorage` on Linux.
173
+ - `secrets doctor`'s "locked" recommendation now explains the KeePassXC
174
+ per-connection-grant limitation (each grant is bound to a short-lived D-Bus connection,
175
+ so a fresh CLI run re-prompts and via keyring usually fails; persistent per-app
176
+ authorization is unimplemented, keepassxc#6458) and steers to a dedicated exposed group
177
+ holding only tg-notes' secrets plus confirmation OFF — the same trust boundary as the
178
+ on-disk session file. Points at `docs/keepassxc.md`.
179
+ - Expanded `AGENTS.md` (TGN-16) with the concrete shipped pieces (the `tg-notes` CLI on
180
+ PyPI + the `tg-notes` / `tg-notes-send` skills) and per-agent distribution notes (Claude
181
+ Code plugin marketplace, OpenCode/OpenClaw `~/.claude/skills` discovery, Hermes import).
182
+ Cross-agent portability confirmed (TGN-D3): the same `SKILL.md` files are read unchanged
183
+ by every Agent-Skills runtime; only distribution differs.
184
+
185
+ ### Fixed
186
+
187
+ - The keyring backend now self-heals `DBUS_SESSION_BUS_ADDRESS` when it is unset. A process
188
+ spawned with a sanitized environment (an MCP host, a cron job) has no
189
+ `DBUS_SESSION_BUS_ADDRESS`, so `secretstorage.dbus_init()` failed with "Environment variable
190
+ DBUS_SESSION_BUS_ADDRESS is unset" and the Secret Service was unreachable. `_ss_collection`
191
+ now calls `_ensure_dbus_env()` before `dbus_init()`: if the variable is unset and the
192
+ standard per-user bus socket (`/run/user/<uid>/bus`) exists, it points the variable there.
193
+ Best-effort — it never raises, and an already-set address always wins.
194
+ - The gated live media test's photo fixture used an invalid 1x1 PNG that Telegram's
195
+ server-side image pipeline rejected with `ImageProcessFailedError`, so
196
+ `TG_NOTES_LIVE=1 pytest tests/test_live_media.py` failed on the photo round-trip. It now
197
+ generates a valid 160x160 8-bit truecolor PNG in-process (signature + IHDR + a
198
+ zlib-compressed IDAT with per-row filter bytes + IEND, all via `struct`/`zlib`, no PIL),
199
+ which round-trips cleanly.
200
+ - The keyring backend now reuses a single `secretstorage` D-Bus connection per process
201
+ (`_ss_collection` lazily opens it once and caches it) instead of opening a new one on every
202
+ vault read. A single command reads the vault several times (`api_hash`, session,
203
+ `has_session`), and KeePassXC binds each per-access-confirmation grant to the requesting
204
+ D-Bus connection — so this makes each command trigger **at most one** KeePassXC confirmation
205
+ prompt instead of one per read.
206
+ - `secrets migrate --to keyring` no longer overwrites the vault with an empty
207
+ (unauthorized) session: `migrate_to_keyring` aborts if the exported session string is
208
+ empty, so a not-logged-in run can't clobber a good vault entry.
209
+ - `contacts set` no longer errors when re-setting a contact to unchanged values: the
210
+ underlying `edit_message` `MessageNotModifiedError` is caught and treated as a successful
211
+ no-op (found while exercising the MCP server).
212
+ - CI: upgrade `pip`/`setuptools` before `pip-audit` so it no longer flags the runner's
213
+ incidental build tooling (`setuptools` PYSEC-2026-3447, absent on 3.12 but present on
214
+ 3.11), which was failing the pipeline. `tg-notes` does not depend on `setuptools`.
215
+
216
+ ## [0.1.0] - 2026-07-24
217
+
218
+ ### Added
219
+
220
+ - Project documentation and rules: `README.md`, `CLAUDE.md` (language and doc-sync
221
+ rules), `docs/architecture.md`, `docs/features.md`.
222
+ - Implementation plan in `TODO.md` (planning phase; no code yet).
223
+ - `GPL-3.0` license (`LICENSE`).
224
+ - Project scaffolding (TGN-1): `pyproject.toml` (CLI `tg-notes`, Telethon dependency,
225
+ editable-installable), `tg_notes` package, argparse command tree (`setup`, `note add`,
226
+ `notes list`, `contacts list/set/remove`, `send`, `notebooks list` — stubs so far),
227
+ local TOML config module (XDG path, `chmod 600`), and `.gitignore` for secrets and
228
+ `*.session`.
229
+ - Autonomous development-workflow rules in `CLAUDE.md` (TDD-first, feature branches,
230
+ Conventional Commits, tag-based SemVer releases, CI expectations, security bar),
231
+ replacing the previous minimal `Git` section.
232
+ - GitHub Actions workflows: CI (`.github/workflows/ci.yml`) running ruff, pytest
233
+ (Python 3.11 and 3.12), bandit, and pip-audit on every push and pull request; and
234
+ release (`.github/workflows/release.yml`) building and publishing to PyPI on `v*` tags.
235
+ - `dev` optional-dependency group in `pyproject.toml` (`pytest`, `pytest-mock`, `ruff`,
236
+ `bandit`, `pip-audit`, `build`).
237
+ - `AGENTS.md` pointing agents to `CLAUDE.md` as the canonical rules and documenting the
238
+ Agent Skills (`SKILL.md`) portability standard.
239
+ - Telegram client layer (TGN-2): `tg_notes/telegram.py` — a synchronous Telethon wrapper
240
+ (`import telethon.sync`) with `build_client` / `connect_authorized` / `whoami` / `login`
241
+ and typed `NotConfiguredError` / `NotAuthorizedError`. Sessions are locked to `chmod 600`
242
+ after login.
243
+ - CLI commands `login` (one-time interactive phone/code/2FA login, stores the session) and
244
+ `whoami` (prints the logged-in account identity as JSON); both report missing credentials
245
+ or an unauthorized session with a clear message and a nonzero exit.
246
+ - Test suite under `tests/` (pytest + pytest-mock): config round-trip / file mode / session
247
+ path, the Telegram layer with Telethon fully mocked (offline), and the CLI argument
248
+ surface. Pytest and ruff config added to `pyproject.toml`.
249
+ - Storage provisioning (TGN-3): `tg-notes setup` creates (or idempotently attaches to) the
250
+ private forum supergroup that stores notes. It ensures the `contacts` topic and a default
251
+ `daily` notebook topic (override with `--notebook`), pins a recovery marker in the group
252
+ so a lost store can be re-discovered, and persists the resolved group id to local config.
253
+ New `telegram.setup` plus helpers (`_resolve_or_create` / `_create_storage_group` /
254
+ `_ensure_topics` / `_pin_marker`) built on Telethon's forum raw API
255
+ (`CreateChannelRequest(megagroup=True, forum=True)`, `CreateForumTopicRequest`,
256
+ `GetForumTopicsRequest`); tests in `tests/test_setup.py` with Telethon fully mocked.
257
+ `setup` drives first-run onboarding itself: when `api_id`/`api_hash` are missing it
258
+ prompts for them and saves them to local config (mode 600); when the device is not
259
+ logged in it runs the interactive `login` (phone → code → 2FA) and retries the
260
+ provisioning. If the prompt is left blank (or the values are unusable) it falls back to
261
+ printing step-by-step manual guidance (my.telegram.org → config path → `chmod 600` →
262
+ `login` → re-run) and exits nonzero.
263
+ - Note capture (TGN-4): `tg-notes note add --notebook <nb> --text-file <f>` appends a note
264
+ to the notebook's forum topic, creating the topic on demand. Reads the note text from a
265
+ file or stdin (`--text-file -`), appends optional `--hashtag TAG` tokens (repeatable) on
266
+ a trailing line, refuses to post an empty note, and prints the posted note as JSON
267
+ (`notebook` / `topic_id` / `message_id` / `date`). New `telegram.note_add` (+ helpers
268
+ `_compose_note` / `_normalize_hashtag`) and a `NotSetUpError` that tells the user to run
269
+ `setup` first; tests in `tests/test_notes.py` with Telethon fully mocked.
270
+ - Note listing (TGN-5): `tg-notes notes list --notebook <nb> [--since <t>]` returns a
271
+ notebook's raw notes as a JSON array (oldest first), each `{message_id, date, text}`,
272
+ skipping the topic-opening service message. `--since` accepts `today`, `HH:MM`,
273
+ `YYYY-MM-DD`, or a full ISO datetime (interpreted as local time when it carries no
274
+ offset) and bounds the messages by date; an unknown notebook yields `[]`. New
275
+ `telegram.notes_list` and a CLI `_parse_since` parser; tests in `tests/test_notes.py`
276
+ and `tests/test_cli.py` (Telethon mocked).
277
+ - Contacts address book (TGN-6): `tg-notes contacts list|set|remove` over the
278
+ message-per-contact schema in the `contacts` topic. New pure `tg_notes.contacts` module
279
+ ((de)serializes the `#contact <key>` block; single-line fields, colons preserved) plus
280
+ `telegram.contacts_list` / `contacts_set` / `contacts_remove`. `set` creates or updates
281
+ in place (only the given fields override an existing record; a new contact requires
282
+ `--chat-id`; edits reuse the same message), `remove` deletes the contact's message
283
+ (missing key is a no-op), `list` returns all contacts as JSON sorted by key. Tests in
284
+ `tests/test_contacts.py` (pure + Telethon-mocked layer). Refactor: shared
285
+ `_resolve_store` helper (in `tg_notes.telegram`) and `_handle_store_errors` /
286
+ `_STORE_ERRORS` (in the CLI) deduplicate the not-set-up / not-configured / not-authorized
287
+ handling across `note add`, `notes list`, and `contacts`.
288
+ - Publish (TGN-7): `tg-notes send --contact <key> --text-file <f>` posts compiled text to a
289
+ contact's chat **as the user** — into a forum topic (`reply_to`) when the contact has a
290
+ `topic_id`, prepending the contact's `mention` on its own line when set. Text comes from
291
+ a file or stdin (`-`); `--dry-run` composes and prints the outgoing message (target,
292
+ topic, text) without sending. New `telegram.send` (+ `_compose_outgoing` /
293
+ `_target_from_chat_id`, resolving `chat_id` as `-100…` int or `@user` / `me`) and a
294
+ `ContactNotFoundError` (CLI exit 5); empty text is refused. Tests in `tests/test_send.py`
295
+ (Telethon mocked). This is the first command that posts outside the storage group.
296
+ - Notebook listing (TGN-8): `tg-notes notebooks list` returns the storage group's notebook
297
+ topics as JSON (`{name, topic_id}`, sorted), excluding the reserved `General` and
298
+ `contacts` topics. New `telegram.notebooks_list` and `RESERVED_TOPICS`; tests in
299
+ `tests/test_notebooks.py`. Completes **Phase 1 (CLI core)** — all deterministic Telegram
300
+ commands are implemented and verified end-to-end. Removed the leftover `_todo` stub
301
+ handler now that no command is a stub.
302
+ - Capture skill (TGN-9): `skills/tg-notes/SKILL.md` — a portable Agent Skill (standard
303
+ `name`/`description` frontmatter) that captures a work note into the Telegram-backed
304
+ store by shelling out to `tg-notes note add`. Two modes: verbatim (user dictates) and
305
+ session summary (composed from real facts — git commits since 00:00, changed files,
306
+ tickets — never invented), 2–6 concise bullets in the user's language, default `daily`
307
+ notebook, optional hashtags. Capture only — it never sends. Verified end-to-end against
308
+ the real account (both modes filed and read back). Succeeds the retired local `report`
309
+ skill, now Telegram-native.
310
+ - Compile & send skill (TGN-10): `skills/tg-notes-send/SKILL.md` — reads notes
311
+ (`tg-notes notes list`), rewrites them per the target contact's `style`, previews with
312
+ `tg-notes send --dry-run`, **always shows a draft and requires an explicit confirmation**
313
+ (the message goes out under the user's own account), then `tg-notes send`. Contacts are
314
+ the single source of chat/topic/mention/style. Verified end-to-end against the real
315
+ account (dry-run → confirmed send to Saved Messages → read back; unknown-contact exit 5).
316
+ Succeeds the retired local `report-send` skill, now driven by the `tg-notes` CLI.
317
+ - Daily-report preset (TGN-11): a first-class section of `skills/tg-notes-send/SKILL.md`
318
+ (`/tg-report`, "отправь дневной отчёт") — the compile & send flow with fixed defaults
319
+ (notebook `daily`, `--since today`), one confirmed send per recipient. Completes
320
+ **Phase 2 (the Claude Code Skill)**: capture (`tg-notes`) + compile & send / daily report
321
+ (`tg-notes-send`), the Telegram-native successor to the old `report`/`report-send` pair.
322
+ - Claude Code plugin packaging (TGN-13): `.claude-plugin/plugin.json` — plugin manifest
323
+ (`name: tg-notes`, version, author, GPL-3.0-or-later) that bundles the two skills
324
+ (auto-discovered from `skills/`). Passes `claude plugin validate . --strict`.
325
+ - Git plugin marketplace (TGN-14): `.claude-plugin/marketplace.json` — single-plugin
326
+ marketplace (`tg-notes-marketplace`) with `source: "./"` (plugin is the repo root).
327
+ Install: `/plugin marketplace add korkin25/tg_notes` then
328
+ `/plugin install tg-notes@tg-notes-marketplace`. The bundled skills drive the `tg-notes`
329
+ CLI, installed separately with `pipx install tg-notes`.
330
+
331
+ ### Changed
332
+
333
+ - Single-source the package version: `pyproject.toml` reads it from
334
+ `tg_notes/__init__.py` (`[tool.hatch.version]`, `dynamic = ["version"]`) so it can no
335
+ longer drift between the two. Bumped to `0.1.0` for the first release.
336
+ - Release workflow (`.github/workflows/release.yml`) now publishes to PyPI via **Trusted
337
+ Publishing** (OIDC, `id-token: write`, `pypi` environment) instead of a stored API
338
+ token, and runs `twine check` on the built artifacts. Publishing still triggers only on
339
+ a `v*` tag; it needs a one-time PyPI Trusted Publisher bound to `korkin25/tg_notes`.
@@ -62,3 +62,22 @@ This project is developed by an AI agent under continuous, autonomous iteration.
62
62
  - High bar: type hints, docstrings, ruff-clean, meaningful tests. Work like a top-tier engineer + DevOps.
63
63
  - Auto-logging: started/ongoing work goes to TODO.md (Current state + phase tables); completed and verified work moves to CHANGELOG.md, in the same change. Never mark a task done without a passing test.
64
64
  - Cold-start: keep the top of TODO.md a "Current state / next action" block so a fresh session knows exactly what to do next.
65
+
66
+ ### Testing in a sandbox (mandatory for live tests)
67
+
68
+ Any test that touches the real config, the real keyring, or Telegram MUST run in an
69
+ **isolated sandbox**, never against your day-to-day install. The real
70
+ `~/.config/tg-notes`, the real keyring (`tg-notes` namespace), and the real storage group
71
+ `-1004432534270` must stay untouched.
72
+
73
+ - Use `scripts/sandbox.py` — `setup` seeds a throwaway config dir (`$TG_NOTES_SANDBOX_DIR`
74
+ or `~/.config/tg-notes-sandbox`, file backend, a copy of the real session) and creates a
75
+ **dedicated test group** (a fresh `-100…` id, never the real one); `run -- <cmd>` runs a
76
+ command against it; `pytest -- <args>` runs the gated live tests (`TG_NOTES_LIVE=1`)
77
+ there; `reset` deletes it. All idempotent.
78
+ - Or follow the manual protocol: prefix every command with `TG_NOTES_CONFIG_DIR=<sandbox>`
79
+ (and `TG_NOTES_KEYRING_SERVICE=tg-notes-sandbox` for keyring tests) against a dedicated
80
+ group. See [docs/sandbox-testing.md](docs/sandbox-testing.md).
81
+ - Unit tests are mocked and already isolated — they must never reach real config, keyring,
82
+ or network. The sandbox session file is a full-access credential: `chmod 600`, outside
83
+ the repo, never committed.
@@ -1,3 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: tg-notes
3
+ Version: 0.1.2
4
+ Summary: Notes → compile → publish, using a private Telegram group as the store and posting under your own account.
5
+ Project-URL: Homepage, https://github.com/korkin25/tg_notes
6
+ Project-URL: Repository, https://github.com/korkin25/tg_notes
7
+ Author: korkin25
8
+ License-Expression: GPL-3.0-or-later
9
+ License-File: LICENSE
10
+ Keywords: cli,notes,telegram,telethon,userbot
11
+ Classifier: Environment :: Console
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Communications :: Chat
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: telethon>=1.44
16
+ Provides-Extra: dev
17
+ Requires-Dist: bandit==1.9.4; extra == 'dev'
18
+ Requires-Dist: build; extra == 'dev'
19
+ Requires-Dist: keyring>=24; extra == 'dev'
20
+ Requires-Dist: mcp>=1.2; extra == 'dev'
21
+ Requires-Dist: pip-audit; extra == 'dev'
22
+ Requires-Dist: pytest-mock; extra == 'dev'
23
+ Requires-Dist: pytest>=8; extra == 'dev'
24
+ Requires-Dist: ruff==0.16.0; extra == 'dev'
25
+ Provides-Extra: keyring
26
+ Requires-Dist: keyring>=24; extra == 'keyring'
27
+ Requires-Dist: secretstorage>=3; (platform_system == 'Linux') and extra == 'keyring'
28
+ Provides-Extra: mcp
29
+ Requires-Dist: mcp>=1.2; extra == 'mcp'
30
+ Provides-Extra: transcribe
31
+ Requires-Dist: faster-whisper>=1; extra == 'transcribe'
32
+ Description-Content-Type: text/markdown
33
+
1
34
  # tg_notes
2
35
 
3
36
  Turn a private Telegram group into your notes store, then publish tailored updates
@@ -60,6 +93,19 @@ The skills drive the CLI, so `tg-notes` must be on `PATH`. The two skills:
60
93
  stored notes per a contact's style, confirm, and publish with `tg-notes send` (daily
61
94
  report is a preset).
62
95
 
96
+ For agent hosts that can't shell out (Claude Desktop, …), a local **stdio MCP server**
97
+ exposes the same core (`note_add` / `note_add_file` / `notes_list` / `contacts_list` /
98
+ `send`). `note_add_file` uploads a local file as a media note — audio auto-transcribes to
99
+ the caption, or the agent passes its own `caption` (e.g. an image description) to skip it:
100
+
101
+ ```bash
102
+ pipx install "tg-notes[mcp]" # then point your MCP client at the `tg-notes-mcp` command
103
+ ```
104
+
105
+ It runs over stdio only — the session and secrets never leave the machine. When the host
106
+ spawns the server with a sanitized environment, the (opt-in) keyring backend self-heals
107
+ `DBUS_SESSION_BUS_ADDRESS` so it can still reach the Secret Service.
108
+
63
109
  Available so far:
64
110
 
65
111
  ```bash
@@ -76,6 +122,24 @@ tg-notes setup [--notebook <name>]
76
122
  tg-notes note add --notebook daily --text-file note.txt --hashtag infra
77
123
  echo "quick note" | tg-notes note add --text-file -
78
124
 
125
+ # Or attach a media file (photo/video/audio/document) as the note — stored as native
126
+ # Telegram media in the topic, kind auto-detected. --caption is the searchable text (any
127
+ # --hashtag is appended to it).
128
+ tg-notes note add --notebook daily --file ~/clips/demo.mp4 --caption "walkthrough" --hashtag demo
129
+
130
+ # Audio notes auto-transcribe: when --file is audio (.ogg/.opus/.mp3/.m4a/.wav/…) and no
131
+ # --caption is given, tg-notes transcribes it LOCALLY and uses the transcript as the caption
132
+ # (searchable text) — best-effort, so if no engine is installed the file still uploads
133
+ # without a caption. On the first transcription with no engine, tg-notes auto-fetches the
134
+ # faster-whisper engine (once, best-effort; disable with transcriber_autoinstall = false).
135
+ # Or pre-install a local engine yourself (both need `ffmpeg` to decode audio):
136
+ # pipx inject tg-notes faster-whisper # the faster-whisper backend
137
+ # # …or set whisper_cmd in config.toml to a whisper.cpp/openai-whisper binary
138
+ # whisper_model (e.g. base/small) picks the model (downloads on first use).
139
+ # Force/skip with --transcribe/--no-transcribe.
140
+ tg-notes note add --notebook daily --file ~/voice/idea.ogg # caption ← transcript
141
+ tg-notes note add --notebook daily --file ~/voice/idea.ogg --no-transcribe
142
+
79
143
  # List the raw notes of a notebook (oldest first) as JSON, optionally bounded by --since
80
144
  # (today | HH:MM | YYYY-MM-DD | ISO datetime; local when no offset). Feeds compilation.
81
145
  tg-notes notes list --notebook daily --since today
@@ -96,6 +160,20 @@ echo "..." | tg-notes send --contact boss --text-file - --dry-run
96
160
  # List the storage group's notebook topics as JSON (excludes General/contacts).
97
161
  tg-notes notebooks list
98
162
 
163
+ # Secrets backend. By default the api_hash + session live in local files (config.toml
164
+ # 600 + *.session). Optionally move them into the OS Secret Service (gnome-keyring /
165
+ # KWallet / KeePassXC — whichever owns org.freedesktop.secrets; the session is stored as
166
+ # a StringSession). `secrets status` shows the active backend and which provider answers.
167
+ pipx install "tg-notes[keyring]"
168
+ tg-notes secrets status
169
+ tg-notes secrets doctor # diagnose the store + get setup/migration steps
170
+ tg-notes secrets migrate --to keyring # or: --to file to move back
171
+ # Using KeePassXC as the store (serve Secret Service, dedicated group, the
172
+ # confirmation trade-off): see docs/keepassxc.md. On Linux + keyring, the CLI re-execs
173
+ # through a named launcher so the vault prompt shows "tg-notes", not "python3.12".
174
+ # Test setup/doctor/migrate end-to-end against a throwaway isolated install
175
+ # (never touches the real config/session/vault): see docs/sandbox-testing.md.
176
+
99
177
  # Log in on its own (setup calls this for you; useful to re-authorize a new device).
100
178
  # Requires api_id/api_hash in local config; writes a chmod-600 session file.
101
179
  tg-notes login
@@ -104,6 +182,10 @@ tg-notes login
104
182
  tg-notes whoami
105
183
  ```
106
184
 
185
+ Omit `--contact` / `--to` / `--notebook` in an interactive terminal to pick from a list
186
+ (uses `fzf`/`sk`/`fzy` if installed, else a numbered menu); with the flag or when piped,
187
+ behavior is unchanged.
188
+
107
189
  ## Security & Telegram ToS
108
190
 
109
191
  - Userbot automation is a **gray area of Telegram's ToS**; you run it on your own