Local CLI for your own Preply tutor and learner accounts. Read-only by default;
the lesson confirmation command is the only mutating path and requires explicit
--confirm --yes flags.
Two ways to reach Preply (global --transport {auto,direct,browser}, default auto):
- direct — unattended. Replays a Preply session stored in 1Password (one item per account) straight to the GraphQL API. No browser, no Touch ID. The session token never touches the repo or disk in plaintext.
- browser — same-origin
fetch()inside a logged-in Chrome tab viabrowser-harness. Stores nothing; the session stays in Chrome.
This is an unofficial community tool. Use it only with accounts you own or are authorized to inspect, and respect Preply's terms and privacy requirements.
Be aware of this up front rather than three commands in:
- Only
tutor-reviewsworks with no setup at all. It reads any tutor's public profile page, needs no login, and runs on any platform. Everything else needs a logged-in Preply session, which means one of the two transports below. directneeds 1Password plus a Service Account bridge (bridge_router) that is not shipped with this package. PointPREPLY_OP_BRIDGE_DIRat your own bridge's scripts directory, or use the browser transport.PREPLY_OP_VAULToverrides the vault name (defaultAgent Automation).browserneedsbrowser-harnessonPATH— an external helper that drives your logged-in desktop Chrome. It is not published as part of this project either.session captureis macOS-only. It decrypts Chrome'sv10cookie store via the macOS Keychain and refuses to run elsewhere. The rest of the CLI is portable; CI runs the suite on Linux and macOS.
Nothing here silently degrades: with neither transport configured, any authenticated command prints both routes and why each is unavailable.
- Install this CLI:
python3 -m pip install -e .- Capture a live Preply session from Chrome into 1Password (present-user, one Keychain prompt). This decrypts each logged-in Chrome profile's session, probes it, and stores the ones that authenticate:
preply session capture
preply session list
preply session status- Run commands unattended — the session comes from 1Password, no browser needed:
preply --role learner account
preply --role learner history --limit 100
preply --role learner confirmation
preply --role tutor status
preply --role tutor students
preply --role tutor analyze
preply tutor-reviews https://preply.com/en/tutor/2807691--transport direct forces the 1Password path (and fails loudly if no matching
session is stored); --transport browser forces the Chrome-tab path; auto
(default) prefers direct and falls back to browser.
Install/expose browser-harness on PATH, open Preply logged-in in Chrome, then:
preply --transport browser --role tutor status
preply --transport browser --role tutor schedule --days 30When several Preply tabs/accounts are open, choose the target before the command:
preply --role tutor account
preply --role learner account
preply --user-id 123456 account
preply --name "Example Learner" accountFor different accounts, save separate snapshots:
preply --role learner snapshot --account-label "Learner" --out data/learner.json
preply --role tutor snapshot --account-label "Tutor" --out data/tutor.json
preply compare data/learner.json data/tutor.jsonsession capture|list|status: manage 1Password-stored sessions for the direct transport.capturedecrypts live Chrome sessions and stores the ones that authenticate (add--profile "Profile 2"to restrict);listshows stored sessions;statusprobes each for current liveness.account: show which account the CLI will use (from the stored session or the logged-in tab).me: one-screen learner dashboard — identity, lifetime stats, hour balance, upcoming lessons, recent lessons, upcoming charge dates, unscheduled paid hours, and tutors awaiting a first payment. Start here.balance: learner hour balance per tutor — hours banked, how many are still unscheduled, how many are tied up in a booked lesson, and the next automatic charge date and billing frequency. A tutoring whose subscription was stopped showsNO_SUBSCRIPTIONwith no charge date: its hours are paid for and will sit idle until booked.renewals: learner money view of subscriptions — what Preply will charge, when, for which tutor, in which currency.balanceanswers "how many hours do I have"; this answers "how much am I about to pay". Only subscriptions that will actually charge are listed: Preply keeps the last charge date and amount on a stopped subscription, so--allis needed to see those. Totals are reported per currency and never summed across them.certificates: learner achievement certificates per subject — hours completed, current and next level, hours to the next level, and the download URL.status: account totals, wallet balance, loaded student revenue total, status counts.students: student list with tutoring id, subject, price, confirmed lessons, hours, revenue, timezone.schedule: upcoming class schedule.wallet: wallet balance, currency, last payout method.messages: message thread summaries and last-message previews.chat [who]: full conversation history with one tutor. Run with no argument to list contacts; pass a name substring or numeric user id to open a thread.--fullprints complete message text plus each attachment's name and URL,--fileslists just the shared materials,--limitpages into older history, and--jsonreturns messages and files together for offline reasoning. Sender is shown asme, the tutor's name,system, orunknown— never guessed (see the note below).history: learner/tutor payment history where Preply exposes it insettings/history.lessons: learner completed-lesson ledger — each row is one real lesson (date, subject, tutor, duration, status, paid amount, rating), with a summary. Unlikehistory(a billing surface), this is an actual per-lesson event ledger.upcoming: learner upcoming lessons (booked lessons and recurrent reservations).tutors: learner active tutors/subscriptions — tutor, subject, price/hour, lessons taken, and subscription (refill) state, sorted by lessons taken.stats: learner lifetime stats — highest lesson streak, lessons completed, practices.confirmation: show the pending "Did your lesson happen?" prompt. Add--confirm --yesto confirm the pending lesson so the tutor can get paid. Use--lesson-id,--expect-tutor, or--expect-datetimeas safety guards.tutor-reviews <url-or-id>: public tutor profile stats, review reasoning, themes, retention, and all reviews by default. Add--limit 5to preview only a few rows. Needs no login — it reads the server-rendered profile page of any tutor.- Each review carries the reviewer's own lesson count and the date of their last lesson, so the output includes a Long-term students table: who stayed, how many lessons they took, and whether they are still taking them. Reviewers are self-selected, so read it as a floor on repeat business, not a census.
student <tutoring_id>: per-student details, statistics, upcoming lessons, past lessons, and revenue history.analyze: summary plus timeline. Add--deepfor per-student past lesson fetching.snapshot: local JSON snapshot for later comparison.compare: compare saved snapshots from different accounts.
- Full chat history does not require Agora.
chatreads message bodies over plain GraphQL; Agora carries the live stream, not the archive. Verified on a real thread: 85 messages spanning 2024-10-06 to 2026-08-06, fetched in two pages, with the final page reporting no more history. - Attachments shared in a conversation (lesson PDFs, audio, homework) are
enumerable with
chat <who> --files: name, MIME type, size, sender, date, and an absolute URL. Preply returns these links root-relative (/files/<id>?download=true), so the CLI makes them absolute; the links still require your logged-in session in the browser. - Chat sender attribution is measured, not inferred.
authorIdis null on about two thirds of messages (system notices and Preply's own action cards), so the command labels each messageme, the tutor's name,system, orunknown, and counts anything unattributable inunknown_senderrather than assigning it to a person. - Public tutor reviews are read from Preply's public server-rendered profile payload. This command does not need a logged-in Chrome tab and can also read a saved public profile HTML/JSON file.
balancecounts hours, not money.hours,unscheduled, andunavailablemap one-to-one onto Preply's owntutoring.hours,unscheduledLessons, andunavailableLessonsfields; Preply owns their exact semantics. Observed live:unavailablehours are excluded fromtotalBalance, so the reported balance and the per-row hour sum can legitimately differ —--jsonreports both (total_balance_hoursandsummed_row_hours) rather than reconciling them.renewalscounts money,balancecounts hours, and they come from different Preply operations.BalanceManagementData(behindbalance) carries refill hours and dates but no amount and no currency, so it structurally cannot answer "how much will I be charged";SettingsTutoringList(behindrenewals) is the only verified source ofchargeAmount. A stopped subscription keeps its final charge date and amount — measured live, 9 of 23 tutorings carried dates from 2023–2025 — so those are excluded from the upcoming total and shown only under--all.- A
--filesnapshot that is valid JSON but not a Preply snapshot is refused, not rendered. Reading one used to print a full report of zeros at exit 0. historyis payment history, not a completed-lesson ledger. Do not use payment-row counts orhours_totalas lesson frequency without a provider-level one-to-one guarantee. For learner cadence, combine an explicitly dated lesson-note source with a Preply snapshot and label each source's freshness.analyze --deepexposes event-level past lessons only when tutor-sidestudent_detailsare present; it is not a learner-side historical lesson export.--filereads an offline snapshot. Include its capture date and account label in any report, and refresh throughaccount/snapshotbefore making a current-state claim.- Lesson confirmation uses Preply's
NextLessonForConfirmationquery andConfirmPastLessonmutation. The default command only displays the pending lesson; mutation requires--confirm --yes. - Exported JSON can contain private student and message data. The
data/directory is ignored by git. - Global account selectors must appear before the subcommand, for example
preply --role learner history, notpreply history --role learner.
python3 -m pip install -e .
python3 -m unittest discover -s testsInstall first. Running python3 -m unittest discover -s tests in a bare clone
collects 21 of 198 tests and reports red — the 15 errors are all one cause
(No module named 'preply_cli'), not 15 findings. Without an install, prefix
with PYTHONPATH=src.
CI runs the suite against the built package on Python 3.11–3.14 across Linux
and macOS, and asserts the collected test count rather than trusting the exit
code: discover exits 0 when it finds nothing.
- The session token is stored only in 1Password (the
Agent Automationvault), never in the repo, a dotfile, an env file, or logs. Thedirecttransport reads it in-process through the 1Password Service Account bridge and never prints it. Thebrowsertransport stores nothing. See SECURITY.md. session captureperforms a one-time, present-user decryption of a live Chrome session (one macOS Keychain prompt). Cookie decryption never runs unattended.- A stored session can go stale (Preply invalidates it server-side on logout).
session statusprobes liveness;directfails loudly rather than guessing. - The CLI is read-only by default. Do not add more mutating commands without a separate review and an explicit confirmation gate.
confirmation --confirm --yesconfirms the pending lesson and can affect tutor payment. Prefer adding--lesson-idand--expect-tutorwhen automating it.- Keep
data/exports local and private.
Commands are wired from a locally-mined catalogue of Preply GraphQL operations and each one is verified against a live account before shipping.
That catalogue is not published here. Shipping this CLI does not require
publishing Preply's private API surface, so the mined operation text is
gitignored and kept local. docs/reverse-engineering-notes.md records the
method and the findings without reproducing the operations themselves.