From 9510300085534972f7d459353ca34cc754140f03 Mon Sep 17 00:00:00 2001 From: Kadin Bullock Date: Sun, 21 Jun 2026 22:22:13 -0600 Subject: [PATCH] fix(session): unbrick MCP under Plaud 30-day tokens; add `plaud refresh` (v0.5.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plaud shortened token lifetimes from ~291 days to 30 days. require()'s TOKEN_REFRESH_BUFFER_SECONDS hard-rejects any token within the buffer of expiry, and that buffer was 30 days — so every freshly-issued token tripped it and the MCP returned session_expired on every call. Drop the buffer to 3 days; move the tray "expiring" warning threshold from 30 to 3 days to match. Also add a `plaud refresh` CLI command that re-auths the stored session, reusing saved email/region (Plaud has no refresh-token grant, so it is a full credential re-auth). No MCP equivalent by design — avoids routing a password through an AI client's context. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 27 ++++++++++++++++++++++++++- pyproject.toml | 2 +- src/plaud_tools/cli.py | 32 ++++++++++++++++++++++++++++++++ src/plaud_tools/session.py | 6 +++++- src/plaud_tools/tray/app.py | 2 +- 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cac3626..10074c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.5.0] - 2026-06-21 + +### Fixed + +- **MCP refused every call (`session_expired`) after Plaud shortened token + lifetimes to 30 days.** `SessionManager.require()` rejects any token within + `TOKEN_REFRESH_BUFFER_SECONDS` of expiry; that buffer was **30 days**, sized + for the old ~291-day tokens. Once Plaud began issuing 30-day tokens, every + freshly-issued token was already inside the buffer, so `require()` raised + `PlaudSessionExpiredError` on every call — even immediately after signing in. + The buffer is now **3 days**, enough lead time to prompt a re-sign-in without + refusing otherwise-valid tokens. The tray's "expiring soon" warning threshold + moved from 30 to 3 days for the same reason (it would otherwise show + permanently on a healthy fresh token). + +### Added + +- **`plaud refresh` CLI command.** Re-authenticates the stored session, + reusing the saved email and region so only the password is needed + (`--email` / `--region` / `--password` override). Plaud has no + refresh-token grant, so this is a full credential re-auth — the same flow as + `plaud login`, minus retyping. No MCP equivalent is provided by design: + routing a password through an AI client's context is avoided. + ## [0.4.1] - 2026-06-15 ### Fixed @@ -1308,7 +1332,8 @@ For full detail see the v0.1.20–v0.1.22 sections below. Headline items: `scripts/plaud_entry.py` wrapper mirrors the existing `plaud_mcp_entry.py` / `plaud_tray_entry.py` pattern. -[Unreleased]: https://github.com/massive-value/plaud-tools/compare/v0.4.1...HEAD +[Unreleased]: https://github.com/massive-value/plaud-tools/compare/v0.5.0...HEAD +[0.5.0]: https://github.com/massive-value/plaud-tools/compare/v0.4.1...v0.5.0 [0.4.1]: https://github.com/massive-value/plaud-tools/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/massive-value/plaud-tools/compare/v0.3.4...v0.4.0 [0.3.4]: https://github.com/massive-value/plaud-tools/compare/v0.3.3...v0.3.4 diff --git a/pyproject.toml b/pyproject.toml index 0a76559..da5e9b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plaud-tools" -version = "0.4.1" +version = "0.5.0" description = "Python rewrite for Plaud CLI and MCP workflows." readme = "README.md" requires-python = ">=3.11" diff --git a/src/plaud_tools/cli.py b/src/plaud_tools/cli.py index 370dff7..62883b1 100644 --- a/src/plaud_tools/cli.py +++ b/src/plaud_tools/cli.py @@ -134,6 +134,17 @@ def build_parser() -> argparse.ArgumentParser: ) login_cmd.add_argument("--region", choices=["us", "eu"], default="us") + # 'refresh' is 'login' with email/region defaulted from the stored session, + # for re-authing an expired/expiring token without retyping them. Plaud has + # no refresh-token grant, so this is still a full credential re-auth. + refresh_cmd = sub.add_parser( + "refresh", + help="Re-authenticate the stored session (reuses saved email/region; prompts for password).", + ) + refresh_cmd.add_argument("--email", help="Override the stored email.") + refresh_cmd.add_argument("--password", help="If omitted, you will be prompted securely.") + refresh_cmd.add_argument("--region", choices=["us", "eu"], help="Override the stored region.") + session_cmd = sub.add_parser("session") session_sub = session_cmd.add_subparsers(dest="session_command", required=True) @@ -198,6 +209,24 @@ def _handle_login( ) +def _handle_refresh( + args: argparse.Namespace, + store: SessionStore, + auth: PlaudAuth | None, +) -> str: + stored = store.load() + email = args.email or (stored.email if stored else None) + if not email: + raise ValueError("No stored email to refresh; run 'plaud login --email ...' instead.") + region = args.region or (stored.region if stored else "us") + password = args.password or getpass.getpass(f"Plaud password for {email}: ") + session = (auth or PlaudAuth(store)).login(email, password, region) + return json.dumps( + {"ok": True, "email": session.email, "region": session.region, "status": "refreshed"}, + indent=2, + ) + + def _handle_session(args: argparse.Namespace, store: SessionStore) -> str: if args.session_command == "set": store.save(PlaudSession(access_token=args.token, region=args.region, email=args.email)) @@ -586,6 +615,7 @@ def _handle_ping(args: argparse.Namespace, client: PlaudClient) -> str: # noqa: # Signature: (args, store, auth) -> str (auth is only used by login) _PRE_CLIENT_HANDLERS: dict[str, Callable[..., str]] = { "login": _handle_login, + "refresh": _handle_refresh, "session": _handle_session, "update": _handle_update, "doctor": _handle_doctor, @@ -632,6 +662,8 @@ def run_cli( # --- Pre-client commands (no PlaudClient needed) --- if args.command == "login": return _handle_login(args, store, auth) + if args.command == "refresh": + return _handle_refresh(args, store, auth) if args.command == "session": return _handle_session(args, store) if args.command == "update": diff --git a/src/plaud_tools/session.py b/src/plaud_tools/session.py index 5b58f5b..fac66cc 100644 --- a/src/plaud_tools/session.py +++ b/src/plaud_tools/session.py @@ -16,7 +16,11 @@ log = logging.getLogger(__name__) -TOKEN_REFRESH_BUFFER_SECONDS = 30 * 24 * 60 * 60 +# Hard gate: require() refuses a token within this window of expiry. Plaud +# now issues 30-day tokens (was ~291 days), so a 30-day buffer rejected every +# freshly-issued token and bricked the MCP. 3 days is enough lead time to +# prompt a re-sign-in without refusing otherwise-valid tokens. +TOKEN_REFRESH_BUFFER_SECONDS = 3 * 24 * 60 * 60 _SECONDS_PER_DAY = 86_400 diff --git a/src/plaud_tools/tray/app.py b/src/plaud_tools/tray/app.py index 26ba07c..f8d4e05 100644 --- a/src/plaud_tools/tray/app.py +++ b/src/plaud_tools/tray/app.py @@ -98,7 +98,7 @@ def _tray_state(self) -> str: days = self._manager.days_until_expiry() if days is None or days == 0: return "expired" - if days <= 30: + if days <= 3: return "expiring" return "signed-in"