Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
32 changes: 32 additions & 0 deletions src/plaud_tools/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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":
Expand Down
6 changes: 5 additions & 1 deletion src/plaud_tools/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion src/plaud_tools/tray/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Loading