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
11 changes: 7 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,13 @@ jobs:

- name: Download ffmpeg (pinned)
run: |
# Pinned to ffmpeg 8.1.1 (essentials build) — same pin as release.yml.
# SHA-256 sourced from https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-8.1.1-essentials_build.zip.sha256
$ffmpegUrl = "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-8.1.1-essentials_build.zip"
$expectedHash = "6F58CE889F59C311410F7D2B18895B33C03456463486F3B1EBC93D97A0F54541"
# Pinned to ffmpeg 8.1.2 (essentials build) — same pin as release.yml.
# NOTE: gyan.dev only hosts the latest version-pinned build; older ones 404,
# so this pin must be bumped whenever gyan.dev publishes a new release
# (was 8.1.1, rotated away 2026-07). Keep release.yml's pin in lockstep.
# SHA-256 sourced from https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-8.1.2-essentials_build.zip.sha256
$ffmpegUrl = "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-8.1.2-essentials_build.zip"
$expectedHash = "DB580001CAA24AC104C8CB856CD113A87B0A443F7BDF47D8C12B1D740584A2EC"
Invoke-WebRequest $ffmpegUrl -OutFile ffmpeg.zip
$actualHash = (Get-FileHash ffmpeg.zip -Algorithm SHA256).Hash
if ($actualHash -ne $expectedHash) {
Expand Down
11 changes: 7 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,13 @@ jobs:

- name: Download ffmpeg
run: |
# Pinned to ffmpeg 8.1.1 (essentials build).
# SHA-256 sourced from https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-8.1.1-essentials_build.zip.sha256
$ffmpegUrl = "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-8.1.1-essentials_build.zip"
$expectedHash = "6F58CE889F59C311410F7D2B18895B33C03456463486F3B1EBC93D97A0F54541"
# Pinned to ffmpeg 8.1.2 (essentials build).
# NOTE: gyan.dev only hosts the latest version-pinned build; older ones 404,
# so this pin must be bumped whenever gyan.dev publishes a new release
# (was 8.1.1, rotated away 2026-07). Keep ci.yml's pin in lockstep.
# SHA-256 sourced from https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-8.1.2-essentials_build.zip.sha256
$ffmpegUrl = "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-8.1.2-essentials_build.zip"
$expectedHash = "DB580001CAA24AC104C8CB856CD113A87B0A443F7BDF47D8C12B1D740584A2EC"
Invoke-WebRequest $ffmpegUrl -OutFile ffmpeg.zip
$actualHash = (Get-FileHash ffmpeg.zip -Algorithm SHA256).Hash
if ($actualHash -ne $expectedHash) {
Expand Down
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.6.0] - 2026-07-04

### Added

- **Summary editing (`edit_summary` MCP tool / `correct-summary` + `set-summary`
CLI).** Previously AI summaries were read-only. You can now fix text in a
generated summary — the reported case being correcting a misspelled name.
`operation="correct"` does a literal, case-sensitive find-and-replace (the
summary counterpart of `correct_transcript`); `operation="replace"` overwrites
the whole summary with new markdown. Backed by Plaud's
`POST /ai/update_note_info`. The recording must already have a generated
summary (this edits the existing note; it does not create one).
- **Folder management (`mutate_folder` MCP tool / `folder create|edit|delete`
CLI).** Previously folders could only be listed and moved between; there was
no way to create, rename/recolor, or delete a folder from the tools.
`mutate_folder` adds all three via Plaud's `POST/PATCH/DELETE /filetag/`.
`edit` sends only the fields you supply (name/color/icon), preserving the
rest. `delete` is irreversible for the folder itself — recordings inside are
kept but become unfiled — so it is gated behind a required `confirm=true`
(MCP) / `--yes` (CLI), mirroring `delete_recording`. New folders default to a
standard icon/color when omitted.

The MCP surface grows from 10 to 12 tools.

## [0.5.0] - 2026-06-21

### Fixed
Expand Down Expand Up @@ -1332,7 +1356,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.5.0...HEAD
[Unreleased]: https://github.com/massive-value/plaud-tools/compare/v0.6.0...HEAD
[0.6.0]: https://github.com/massive-value/plaud-tools/compare/v0.5.0...v0.6.0
[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
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The Python rewrite is complete. The active code lives under `src/plaud_tools/`:

- `client.py` — Plaud domain/client layer (auth, session, all API flows)
- `cli.py` — Python CLI (`plaud` / `pld` entry points)
- `mcp.py` — MCP handler functions (10 tools: browse, get, mutate, delete, rename_speaker, correct_transcript, upload, process, list_folders, merge_recordings)
- `mcp.py` — MCP handler functions (12 tools: browse, get, mutate, delete, rename_speaker, correct_transcript, edit_summary, upload, process, list_folders, mutate_folder, merge_recordings)
- `server.py` — Python MCP server process (`plaud-mcp` entry point, stdio transport)

The TypeScript prior art has been removed. The `har-captures/` directory contains live Plaud API traffic captures (gitignored — local reference only).
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,17 @@ Open the tray menu, click **Uninstall…**, review the checklist, and click **Un

## What PlaudTools can do

PlaudTools gives your AI assistant seven tools for working with your Plaud account. You don't call these directly — you ask in plain English, and the assistant picks the right one.
PlaudTools gives your AI assistant a set of tools for working with your Plaud account. You don't call these directly — you ask in plain English, and the assistant picks the right one.

| What it does | What you'd ask |
|---|---|
| Find recordings | *"Show me my recordings from last week."* |
| Read a recording | *"What did I say in the Henderson meeting?"* |
| Rename, move, or trash | *"Rename yesterday's 9am recording to 'Tax planning call'."* |
| Fix a transcript or summary | *"Fix the spelling of the client's name in yesterday's summary."* |
| Upload audio | *"Upload this audio file and transcribe it."* (with attachment) |
| Transcribe and summarize | *"Transcribe and summarize yesterday's recording."* |
| List folders | *"What folders do I have in Plaud?"* |
| List and manage folders | *"Create a 'Client Calls' folder"* / *"What folders do I have in Plaud?"* |
| Merge recordings | *"Merge these three call segments into one recording."* |

## Other ways to install
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.5.0"
version = "0.6.0"
description = "Python rewrite for Plaud CLI and MCP workflows."
readme = "README.md"
requires-python = ">=3.11"
Expand Down
86 changes: 86 additions & 0 deletions src/plaud_tools/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,24 @@ def build_parser() -> argparse.ArgumentParser:

folders_cmd = sub.add_parser("folders") # noqa: F841 # side-effect: registers subparser

folder_cmd = sub.add_parser("folder", help="Create, edit, or delete folders.")
folder_sub = folder_cmd.add_subparsers(dest="folder_command", required=True)

folder_create = folder_sub.add_parser("create")
folder_create.add_argument("name")
folder_create.add_argument("--color", help="Hex color, e.g. '#4c8eff'")
folder_create.add_argument("--icon", help="Icon glyph codepoint, e.g. 'e627'")

folder_edit = folder_sub.add_parser("edit")
folder_edit.add_argument("folder_id")
folder_edit.add_argument("--name")
folder_edit.add_argument("--color")
folder_edit.add_argument("--icon")

folder_delete = folder_sub.add_parser("delete")
folder_delete.add_argument("folder_id")
folder_delete.add_argument("--yes", action="store_true")

move_to_folder_cmd = sub.add_parser("move-to-folder")
move_to_folder_cmd.add_argument("recording_id")
move_to_folder_cmd.add_argument("folder_id")
Expand All @@ -75,6 +93,19 @@ def build_parser() -> argparse.ArgumentParser:
correct_transcript_cmd.add_argument("find")
correct_transcript_cmd.add_argument("replace")

correct_summary_cmd = sub.add_parser("correct-summary")
correct_summary_cmd.add_argument("recording_id")
correct_summary_cmd.add_argument("find")
correct_summary_cmd.add_argument("replace")

set_summary_cmd = sub.add_parser("set-summary")
set_summary_cmd.add_argument("recording_id")
set_summary_group = set_summary_cmd.add_mutually_exclusive_group(required=True)
set_summary_group.add_argument("--content", help="New summary markdown")
set_summary_group.add_argument(
"--content-file", help="Path to a file containing the new summary markdown"
)

transcribe_cmd = sub.add_parser("transcribe")
transcribe_cmd.add_argument("recording_id")
transcribe_cmd.add_argument("--template")
Expand Down Expand Up @@ -409,6 +440,29 @@ def _handle_folders(args: argparse.Namespace, client: PlaudClient) -> str: # no
)


def _handle_folder(args: argparse.Namespace, client: PlaudClient) -> str:
def _shape(tag: Any) -> dict[str, Any]:
return {"id": tag.id, "name": tag.name, "color": tag.color, "icon": tag.icon}

if args.folder_command == "create":
tag = client.create_folder(args.name, color=args.color, icon=args.icon)
return json.dumps({"ok": True, "action": "create", "folder": _shape(tag)}, indent=2)
if args.folder_command == "edit":
if args.name is None and args.color is None and args.icon is None:
raise ValueError("folder edit requires at least one of --name, --color, --icon")
tag = client.update_folder(args.folder_id, name=args.name, color=args.color, icon=args.icon)
return json.dumps({"ok": True, "action": "edit", "folder": _shape(tag)}, indent=2)
if args.folder_command == "delete":
if not args.yes:
raise ValueError(
f"Deleting folder {args.folder_id!r} cannot be undone (recordings inside are kept "
f"but become unfiled). Re-run with --yes to confirm."
)
client.delete_folder(args.folder_id)
return json.dumps({"ok": True, "action": "delete", "folder_id": args.folder_id}, indent=2)
raise AssertionError(f"unhandled folder command: {args.folder_command}")


def _handle_move(args: argparse.Namespace, client: PlaudClient) -> str:
folder_id = None if args.folder_id == "-" else args.folder_id
client.set_recording_folder(args.recording_id, folder_id)
Expand Down Expand Up @@ -485,6 +539,35 @@ def _handle_correct_transcript(args: argparse.Namespace, client: PlaudClient) ->
)


def _handle_correct_summary(args: argparse.Namespace, client: PlaudClient) -> str:
result = client.correct_summary(args.recording_id, args.find, args.replace)
return json.dumps(
{
"ok": True,
"recording_id": args.recording_id,
"find": args.find,
"replace": args.replace,
"replacements": result["replacements"],
},
indent=2,
)


def _handle_set_summary(args: argparse.Namespace, client: PlaudClient) -> str:
if args.content_file:
path = Path(args.content_file)
if not path.exists():
raise ValueError(f"file not found: {args.content_file}")
content = path.read_text(encoding="utf-8")
else:
content = args.content
client.set_summary(args.recording_id, content)
return json.dumps(
{"ok": True, "recording_id": args.recording_id, "mutation": "set-summary"},
indent=2,
)


def _handle_upload(args: argparse.Namespace, client: PlaudClient) -> str:
import tempfile

Expand Down Expand Up @@ -631,6 +714,7 @@ def _handle_ping(args: argparse.Namespace, client: PlaudClient) -> str: # noqa:
"summary": _handle_summary,
"rename": _handle_rename,
"folders": _handle_folders,
"folder": _handle_folder,
"move-to-folder": _handle_move,
"move": _handle_move,
"trash": _handle_trash,
Expand All @@ -640,6 +724,8 @@ def _handle_ping(args: argparse.Namespace, client: PlaudClient) -> str: # noqa:
"trash-restore": _handle_trash_restore,
"rename-speaker": _handle_rename_speaker,
"correct-transcript": _handle_correct_transcript,
"correct-summary": _handle_correct_summary,
"set-summary": _handle_set_summary,
"upload": _handle_upload,
"merge": _handle_merge,
"transcribe": _handle_transcribe,
Expand Down
Loading
Loading