npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@opentrain-ai/cli

v0.23.0

Published

Agent-first command-line client for the OpenTrain public API: author and assign LMS retraining, author/publish adaptive job Task Checklists (file-first machine envelope), run employer-only Reviewer Feedback review/triage/access/settings, and manage jobs,

Downloads

1,460

Readme

OpenTrain CLI

Command-line client for the OpenTrain public API. Built for coding agents and scripts: register an agent account, draft and publish jobs, review proposals, hire freelancers, manage contracts and milestones, message, poll updates or register webhooks, and manage credits and your team — all from the terminal. The same CLI also maintains the canonical Instructions pages for a job, builds and assigns Project To-dos and quizzes, runs the complete Training/LMS authoring and review lifecycle, and drives DEV-2741 selective quiz coaching for employers: todos quiz coaching … (queue/state/threads reads, draft feedback, redo selection, accept, planned + confirmed send-back), plus the distinct FULL-retake todos quiz remediate plan|apply lane. Every coaching subcommand answers --help with its focused contract; every mutation demands a caller-supplied stable --idempotency-key. Agents can also discover the live contract, filter/export coaching work, and attach private feedback images entirely from the CLI. todos quiz coaching image upload is the safe default; image download --out streams, verifies, and atomically places a worker image so the calling agent can inspect it without exposing a signed storage grant.

Freelancer API keys are deliberately read-only: they can search public jobs and read the owner's authorized messages. They cannot send messages, answer or submit quizzes, use coaching actions, or mutate account data.

Money never moves from the CLI: hire, milestone funding/approval, and contract-end requests record a pending approval and return an approvalUrl that a signed-in human must open and confirm in the OpenTrain app.

Reusable private video library

CLI 0.11+ exposes the rollout-gated opentrain videos family. Agents can list and inspect organization-owned videos, upload a local video directly through a one-time resumable grant, poll/cancel processing, record draft Instructions or LMS placement references, prepare/download an MP4 rendition, and fetch playback metadata. Run opentrain videos --help for the complete command list.

Video bytes never pass through OpenTrain JSON. videos download grant|save requires an API token explicitly minted with videos:download. Recording a reference does not insert a TipTap/LMS node or publish a worker-visible pin; those are separate, atomic product mutations.

Install

npm install -g @opentrain-ai/cli
opentrain --help

Or run without installing:

npx -y @opentrain-ai/cli@latest whoami

When running from a monorepo checkout that already has an @opentrain-ai/cli workspace, include @latest so npm resolves the published package instead of an unbuilt local workspace.

Quick start (agent onboarding — no human account needed)

# 1. Create an anonymous agent account with a pre-claim API token
#    (job drafting/publishing + read access). Saved to local config.
opentrain auth register --agent-name "My Agent"

# 2. Draft a job from a plain-text description — OpenTrain parses it
opentrain jobs draft create --description-file ./job-description.txt

# 3. Fill any missing fields the draft output asks for
opentrain jobs draft update --job-id <id> --set pricePerHour=25

# 4. Publish when the draft is publish ready
opentrain jobs publish --job-id <id>

# 5. To unlock hiring, messaging, and team invites, hand the account to a human:
opentrain auth claim --email [email protected]
opentrain auth claim-status --wait

Already have a personal API token (ot_pat_…) from the OpenTrain app? Create it from Employer Settings -> Developer -> API keys, then copy it once:

opentrain auth login --api-key ot_pat_<publicId>_<secret>
opentrain whoami

Commands

opentrain --version
opentrain whoami [--json]
opentrain auth register [--agent-name <name>] [--org-name <name>] [--base-url <url>] [--force] [--json]
opentrain auth claim --email <human-email> [--claim-token <token>] [--json]
opentrain auth claim-status [--wait] [--timeout <seconds>] [--claim-token <token>] [--json]
opentrain auth login --api-key <token> [--base-url <url>]
opentrain auth status [--json]
opentrain auth logout
opentrain jobs draft create --description <text> [--title <title>] [--external-id <id>] [--idempotency-key <key>] [--json]
opentrain jobs draft create --description-file <path> [--title <title>] [--external-id <id>] [--idempotency-key <key>] [--json]
opentrain jobs draft create --canonical-file <path> [--external-id <id>] [--idempotency-key <key>] [--json]
opentrain jobs draft create --payload-file <path> [--external-id <id>] [--idempotency-key <key>] [--json]
opentrain jobs draft update --job-id <id> --set <field=value> [--set <field=value> ...] [--json]
opentrain jobs draft update --job-id <id> --patch-file <path> | --patch-json <json> [--json]
opentrain jobs list [--status <status>] [--limit <n>] [--cursor <cursor>] [--json]
opentrain jobs search [--q <text>] [--category <slug>] [--language <lang>] [--country <iso>] [--pay-type PAY_PER_HOUR|FIXED_PRICE|PAY_PER_LABEL] [--limit <n>] [--cursor <cursor>] [--json]
opentrain jobs publish --job-id <id> [--json]
opentrain jobs invite --job-id <id> --freelancer-id <id> [--json]
opentrain jobs invitations list --job-id <id> [--freelancer-id <id>] [--state INVITED|DECLINED|APPLIED|HIRED] [--limit <n>] [--cursor <cursor>] [--all] [--pretty]
opentrain jobs invitations get --job-id <id> --freelancer-id <id> [--pretty]
opentrain jobs close --job-id <id> [--json]
opentrain jobs update-published --job-id <id> --set <field=value> [--set <field=value> ...] [--json]
opentrain jobs update-published --job-id <id> --patch-file <path> | --patch-json <json> [--json]
opentrain contracts list [--job-id <id>] [--status active|ended] [--limit <n>] [--cursor <cursor>] [--all] [--json]
opentrain contracts get --contract-id <id> [--json]
opentrain contracts end --contract-id <id> [--json]
opentrain milestones create --contract-id <id> --description <text> [--name <name>] [--amount <usd>] [--volume <n>] [--due-date <iso>] [--json]
opentrain milestones fund --milestone-id <id> [--json]
opentrain milestones approve --milestone-id <id> [--json]
opentrain approvals get --approval-id <id> [--json]
opentrain proposals list --job-id <id> [--status <status>] [--limit <n>] [--cursor <cursor>] [--json]
opentrain proposals get --proposal-id <id> [--interview] [--json]
opentrain proposals hire --proposal-id <id> --amount <usd> [--milestone-name <name>] [--milestone-description <text>] [--due-date <iso>] [--confirm-not-fit-override] [--json]
opentrain freelancers get --id <user-id-or-slug> [--json]
opentrain talent search [--q <text>] [--skill <s>] [--language <l>] [--country <ISO>] [--badge VERIFIED_TRAINER] [--talent-type Individual|Agency] [--availability <text>] [--min-reputation <n>] [--tier PROVEN|ESTABLISHED|PROVISIONAL] [--available-now] [--sort relevance|reputation|rate_asc|rate_desc|recent] [--limit <n>] [--offset <n>] [--json]
opentrain talent reputation --slug <profile-slug-or-user-id> [--json]
opentrain messages list [--filter all|job|proposal] [--unread-only] [--limit <n>] [--cursor <cursor>] [--json]
opentrain messages unread [--filter all|job|proposal] [--limit <n>] [--cursor <cursor>] [--json]
opentrain messages read --conversation-id <id> [--limit <n>] [--direction older|newer] [--cursor <cursor>] [--json]
opentrain messages send --conversation-id <id> --content <text> [--json]
opentrain messages send --conversation-id <id> --content-file <path> [--json]
opentrain messages edit --message-id <id> --expected-revision <n> --idempotency-key <key> (--content <text> | --content-file <path|->) [--json]
opentrain messages delete --message-id <id> --expected-revision <n> --idempotency-key <key> [--json]
opentrain messages start-proposal-thread --proposal-id <id> [--json]
opentrain updates poll [--cursor <cursor>] [--limit <n>] [--json]
opentrain credits show [--json]
opentrain credits ledger [--cursor <cursor>] [--limit <n>] [--json]
opentrain credits top-up --amount <usd> [--json]
opentrain credits top-up-status --top-up-id <id> [--json]
opentrain webhooks create --url <url> --events <type,...> [--json]
opentrain webhooks list [--json]
opentrain webhooks get <webhook-id> [--json]
opentrain webhooks delete <webhook-id> [--json]
opentrain tokens list [--json]
opentrain tokens revoke --token-id <id> [--json]
opentrain team show [--json]
opentrain team invite --email <address> [--json]
opentrain payments pending [--json]
opentrain capabilities [--json]
opentrain instructions help
opentrain todos help
opentrain lms help
opentrain manager help

Pass --json on any command for the exact API response body (best for agents and scripts).

Shared job operations (manager)

opentrain manager help (CLI 0.11.0+) is the context-first control surface for managing an existing employer job alongside other people and agents. One manager context get --view compact call automatically carries the top-10 fresh priority attention signals with their derived claim state, signal counts, per-rule evaluator health, the effective typed policies, and the operating-guide digest — there is no second bootstrap command.

Every manager mutation (CLI 0.16.0+) has command-specific discovery: leaf --help (e.g. opentrain manager actions create --help) prints that operation's exact required/optional body fields with types, enums, and bounds, the dedicated-flag partition, evidence guidance, and a copy-paste example accepted by the real server validator; --print-schema on the same commands prints the machine-readable opentrain-cli-schema/1 envelope with zero authentication and zero network. Schema-discovery-by-production-400 is over.

CLI 0.11.0 automatically keeps every covered job-scoped call synchronized. It stores a server-minted session credential and the latest per-job sync tokens in a private, per-agent-instance state file, then threads them without prompting. Every covered JSON response includes sync and syncCoverage; inspect sync.conflicts, sync.recommendations, and sync.nextOperations before the next action. Raw credentials and signed tokens never appear in CLI output. Deleting local sync state is safe—it only causes an evidence-backed bootstrap. syncCoverage.coverage is exhaustive: native and bridged calls refresh shared context, while none explains why that operation does not. A durable agent instance is taken only from PASEO_AGENT_ID or OPENTRAIN_AGENT_INSTANCE; without one the CLI safely stays ephemeral instead of accidentally sharing state through a working directory. Ephemeral covered calls additionally receive the advisory-only recommendation.establish_durable_agent_session envelope entry naming the exact fix (export a unique per-agent-instance value BEFORE running commands); never reuse one instance value across concurrent agents — that would share a cursor.

  • manager sync poll waits up to 25 seconds for a fresh shared-context update; --watch emits one bounded JSON response per poll.

  • manager intents declare|list|resolve is the write-ahead coordination lane. Declare before spontaneous work so another manager sees ownership and a duplicate agent receives the live winner instead of racing.

  • manager proposals submit|list|review|withdraw records a non-enforced alternative when the requested workflow conflicts with current team policy, claims, or intent. Reviewing a proposal never executes its suggested change.

  • manager overlays get|set keeps guidance at its correct scope: TEAM is owner-controlled shared guidance, PERSONAL is one manager's durable memory, and SESSION is one agent instance's scratch memory. Overlays never change canonical gates or policies.

  • manager messages send is the synchronized live-job send path. It requires a stable --idempotency-key and the complete claimed-action guard, including the exact latest authorized root/thread message id (or literal none only when the conversation was observed empty). The server resolves the canonical Job Message or Proposal Message job, commits the message and context advance atomically, and refuses stale or cross-job sends. It requires both messages:manage and messages:write; ordinary messages send remains the backward-compatible unsynchronized path.

  • manager signals list|get|acknowledge|snooze|dismiss|claim — deterministic attention signals, highest priority first. claim is the ONLY path that turns a signal into owned work (triage never claims) and requires the exact --expected-signal-revision; eligibility is ACTIVE + FRESH + effective OPEN/ACKNOWLEDGED, non-shadow.

  • manager policies effective|list|record|verify-enforcement — typed, attributed, superseding durable policies (never chat or free text).

  • manager guide get [--format json|markdown] — the generated read-only Job Operating Guide (guideVersion + SHA-256 checksum). The same Markdown is the MCP resource opentrain://jobs/{jobId}/operations/guide.

  • manager setup status|plan|apply (CLI 0.15.0+) — the guided operational setup flow. Shared setup is product-driven: while a job's operating brief, typed workflow, or an applicable required operating choice is missing, every covered response's sync envelope carries the deterministic recommendation.job_setup_incomplete entry pointing here. The DRIVEN multi-agent flow runs in order: status (machine packet + inProgress view) → manager intents declare … --intent-key setup.guided (a DUPLICATE names the live winner but does not transfer ownership) → plan … --adopt-intent <yourIntentId> → human approval → apply … --confirm-live → verify, resolve your intent, hand off. plan composes the exact canonical writes ZERO-WRITE with a deterministic planReceipt, never guesses an unanswered required choice, and fails closed on a truncated inProgress view, foreign active setup.* intents/proposals, or INDETERMINATE evidence (follow the packet's evidenceRecovery read). apply strictly re-validates the whole plan against the closed guided-setup grammar before ANY request leaves the process — exactly the canonical brief publish and guided typed-policy records for this job, exact paths, content-bound idempotency keys, no unknown fields or duplicates; the planReceipt is integrity/approval evidence, never authority. It also fails closed without --confirm-live, on shared-context drift, on INDETERMINATE evidence, and on foreign setup.* coordination. Both plan and apply prove ownership by exact durable-session identity: the intent's sessionId must equal the current response's sync.session.sessionId with a matching principal. A winner ID visible in a DUPLICATE result is never adoptable, and a copied plan cannot be applied from another session. The command then executes and prints the verified shared state. Once setup reports COMPLETE or DEFERRED, stop prompting — completion is shared across the employer team. An approved postponement is an ordinary OPERATING_RULE.setup_deferral_* typed policy.

  • manager work definitions|executions|migrations … (CLI 0.23.0+, DEV-3191) — the dynamic per-job Work Catalog. Work is PER-JOB catalog data, never a platform enum: every covered response's sync.workCatalog teaches the catalog head/status, the winner-only setup lease, pending human approvals, and this job's work-coordination mode. Definition-bound mutations present the FULL binding (versionNumber + contentHash + coordinationSignature); every execution mints ONE coordinating action that is the single claim authority. An INFERRED proposal never activates without the typed WORK_DEFINITION_APPROVAL.<definitionKey> policy recorded by a non-agent human job manager, after every possible-duplicate candidate needing a decision is resolved (WORK_DUPLICATE_RESOLUTION.<first>.<second>; advisory same-shape evidence never blocks).

  • Work-coordination mode (sync.workCatalog.coordination): the singleton typed policy WORK_COORDINATION_MODE — recorded only by a non-agent human job manager — declares how strictly this job requires declared work: OPEN (default when no row exists; the catalog is advisory), COORDINATED (every guarded mutation such as manager messages send must present a claimed action bound to an OPEN work execution, defined or provisional), MANAGED (that execution must be DEFINED work under an ACTIVE definition). A null mode is fail-closed evidence (no current head, a branched lineage, or an unreadable head) — never OPEN. A refused guarded mutation is a 409 whose details carry reason: JOB_OPS_WORK_COORDINATION_REQUIRED, requirement (WORK_EXECUTION_REQUIRED | DEFINED_WORK_REQUIRED | MODE_UNREADABLE), and mode; the JSON error envelope passes them through unchanged.

  • External attestations (manager observations record with authority OBSERVED and a top-level externalAttestation): the catalog cannot see browser edits, chat/ticket/spreadsheet tools, or out-of-band mutations, so that state is UNKNOWN unless a CURRENT attestation covers it. ATTEST carries a bounded per-job source.system slug, optional locator/subject, a claim, a non-future attestedAt, a REQUIRED expiresAt (≤ 30 days), and an optional supersedesObservationId; INVALIDATE names the attestation a webhook, UI, agent, or human retired. Attribution is canonical: pass the top-level actorAgentLabel (stored as createdByAgentText); a nested label is refused. The server proves the same-context lineage before writing (readable non-future target, same source + subject, one live ender per target, ONE current head per source + subject — a second root is refused and told which head to supersede). manager guide get renders externalCoverage: per-entry status, attribution, bounded evidence, and the ending receipt (who ended it, why, when, evidence).

When a message suggests repeated confusion, that is an agent inference—not a deterministic OpenTrain alert. Record it as INFERRED, show a zero-write LMS retraining outline, and obtain explicit human approval before creating or assigning retraining. Recommendations and proposals are never write authority.

These commands require an OpenTrain server exposing the job_operations.signals.* / job_operations.policies.* / job_operations.guide.get operations (opentrain capabilities lists them); the base manager family needs CLI 0.8.0+, the attention/policy/guide commands need CLI 0.10.0+, automatic sync/intents/proposals/overlays need CLI 0.11.0+, guided setup (manager setup) needs CLI 0.15.0+, and the Work Catalog, coordination modes, and external attestations need CLI 0.23.0+.

Canonical job instructions

opentrain instructions reads and edits the same pages shown in the actual job Instructions tab. A job ID is mandatory; there are no standalone workspaces, manuals, bindings, or publication copies.

opentrain instructions inspect --job <job-id> --json
opentrain instructions page get --job <job-id> --page <page-id> --json
opentrain instructions page link --job <job-id> --page <page-id> \
  --anchor <stable-node-id> --json
opentrain instructions page preview --job <job-id> \
  --content-file ./handbook.tiptap.json --json
opentrain instructions page create --job <job-id> --title "Reviewer handbook" \
  --key reviewer-handbook-v1 --content-file ./handbook.tiptap.json \
  --confirm-live --json
opentrain instructions import docx --job <job-id> --file ./handbook.docx \
  --title "Reviewer handbook" --key reviewer-handbook-docx-v1 --confirm-live --json
opentrain instructions page patch --job <job-id> --page <page-id> \
  --expected-checksum <sha256> --ops-file ./patch.json --confirm-live --json
opentrain instructions verify --job <job-id> --json
opentrain instructions visual-check --job <job-id> --json

# Controlled multi-page work: checkout → edit files → diff/validate → immutable
# plan → review plan JSON → one atomic canonical-draft tree swap.
opentrain instructions checkout --job <job-id> --dir ./job-instructions --json
opentrain instructions diff --dir ./job-instructions --json
opentrain instructions validate --dir ./job-instructions --json
opentrain instructions plan --dir ./job-instructions --out ./job-instructions.plan.json --json
opentrain instructions apply --plan ./job-instructions.plan.json --confirm-live --json
opentrain instructions verify --job <job-id> --against-bundle ./job-instructions --json

Every page reports its contentSchemaVersion. Schema v1 retains the original job-document node set; schema v2 adds details, callouts, YouTube, audio, and attachment blocks. Upgrade an existing page explicitly, then re-check it out before authoring v2 content:

# No-write status and impact plan
opentrain instructions page upgrade-schema --job <job-id> --page <page-id> --json

# One-way v1 → v2 upgrade. --wait follows the quiescence window to completion.
opentrain instructions page upgrade-schema --job <job-id> --page <page-id> \
  --target 2 --confirm-live --wait --json

# Convert a supported YouTube URL to the canonical safe TipTap node offline.
opentrain instructions youtube-node --url "https://youtu.be/jNQXAC9IVRw" \
  --title "Project walkthrough" --json

Fresh checkouts use bundle format opentrain-job-instructions-bundle/2 and record the schema version of every page. Older version-blind bundles fail closed; run a fresh checkout instead of hand-editing their manifest. YouTube URLs are normalized to the privacy-enhanced player and arbitrary iframe HTML, playlists, tracking parameters, and non-YouTube hosts are rejected.

Job instruction writes change the canonical employer draft. Create, replace, and DOCX import preview without writing until you add --confirm-live; every other draft mutation requires that flag. Before a shared-draft write, the CLI obtains a short-lived signed preflight token bound to the exact job, actor, operation, content, and canonical state. The server revalidates it, then the CLI reads the canonical page back and verifies its checksum or removal. Every successful operation returns the exact employer URL where the result is visible.

Checkout directories are non-authoritative working copies. plan fails on remote drift and records the exact archive set and normalized content reviewed by the operator. apply stages deterministic TipTap documents and swaps the entire page tree in one database transaction, so workers never see a partially assembled multi-page manual. Interrupted retries converge using the same idempotency key; regenerate the plan instead of editing plan JSON.

Pages support roots plus one subpage level, matching the job editor. DOCX import converts headings, lists, tables, formatting, links, and embedded images deterministically. Preview returns the embedded-image manifest and stable final URL plan without uploading; confirmed import uploads those exact bytes and inserts the canonical URLs. Direct image uploads accept JPEG, PNG, WebP, GIF, and AVIF up to 5 MB. Whole-document replacement additionally requires the last-read checksum and --confirm-replace; normal rich edits use checksum-guarded node patches. Run opentrain instructions page patch help for the six stable-node operations and examples.

visual-check returns the authenticated employer URL, exact sidebar order, page checksums, expected images, viewports, and screenshot assertions. It does not pretend an employer token proves the worker reader: release verification of that surface requires a real test worker/contract instruction session.

Draft history exposes the employer-only page checkpoints captured automatically on every canonical draft write (and by sweep recovery), plus explicit manual checkpoints and a destructive restore:

opentrain instructions history --job <job-id> --page <page-id> --json
opentrain instructions history show --job <job-id> --page <page-id> \
  --checkpoint <checkpoint-id> --json
opentrain instructions history compare --job <job-id> --page <page-id> \
  --checkpoint <checkpoint-id> --to current --json
opentrain instructions history checkpoint --job <job-id> --page <page-id> \
  --key <idempotency-key> --json
opentrain instructions history restore --job <job-id> --page <page-id> \
  --checkpoint <checkpoint-id> --expected-checksum <sha256> \
  --key <fresh-idempotency-key> --confirm-restore --confirm-live --json

Listing uses opaque page-bound cursors (--cursor, from nextCursor). Compare diffs the checkpoint against the current draft (or another checkpoint): ADDED blocks exist only in the current draft and would be removed by a restore; REMOVED blocks would be brought back. Restore is destructive: it replaces the live page content, so it refuses to run without --expected-checksum from a fresh compare plus both --confirm-restore and --confirm-live, always records a durable undo baseline checkpoint first, and returns 409 if the live page changed since the checksum was read — compare again, take the fresh checksum, and retry with a NEW --key. After success the CLI verifies storage + checksum readback only — never collaboration sync or browser rendering; confirm visually in the job Instructions tab.

For accounts in the immutable-release rollout, workers read snapshots captured from the canonical job draft. Prepare, preview, and publish are explicit sequential operations; publishing requires a delivery policy on every surface:

opentrain instructions release prepare --job <job-id> --json
opentrain instructions release preview --job <job-id> --candidate <candidate-id> --json
opentrain instructions release publish --job <job-id> --candidate <candidate-id> \
  --operation-key <key> --delivery silent|notify|read-required --confirm --json
opentrain instructions release status --job <job-id> --json

Candidates expire after 30 minutes. silent changes the effective release without a worker notification, notify sends a lightweight notification, and read-required creates an acknowledgement obligation plus reminders. The release records the server recommendation and any employer override. Actors outside the rollout receive 404 from release commands and remain on the existing immediately-live Instructions behavior.

Training/LMS

opentrain lms works on the same canonical course draft as the dashboard. A dashboard-owned and CLI-owned copy never exist. Capability discovery tells an agent the active shared TipTap profile, registry version, per-block author/edit/render support, private-asset semantics, active schema, asset limits, assessment settings, job linking support, and Training controls before it generates a bundle. Lesson contentDoc values are canonical TipTap JSON; agents must not generate HTML or a CLI-only content model:

opentrain lms capabilities --json
opentrain lms courses create --title "Reviewer onboarding" \
  --origin-job <job-id> --key reviewer-onboarding-v1 --json
opentrain lms courses recover-create --key reviewer-onboarding-v1 --json

LMS reads also return canonical human-navigation links[]. Text mode prints them under Canonical links:; --json preserves each link's kind, audience, URL, and scoped identifiers. Send EMPLOYER links to employers (job Training, job-scoped course/preview/version/module/lesson/review/release impact) and only returned LEARNER links to assigned workers (their Manage Job → Project To-dos shell). Do not reconstruct routes from IDs or replace the server-returned origin. URLs contain no credentials and are reauthorized when opened; locked future learner lessons are not returned.

The safe hybrid authoring loop is checkout → edit → diff → reconcile when the dashboard changed → validate → push. Pull only fast-forwards a locally clean bundle; reconcile merges non-overlapping local and dashboard edits and refuses true conflicts. Push checks the remote draft revision before any asset upload and again before the draft mutation. The dashboard can reopen and edit every current-profile block emitted by the CLI, and checkout/push preserves those blocks, marks, attributes, ordering, and asset IDs without conversion through the compact Native Forms editor.

Courses can persist an advisory authoringIntent in course.yaml and the canonical draft tree. It controls authoring validation and worker-equivalent Preview defaults without becoming a second delivery engine. Legacy courses default to STANDARD + SYSTEM; INDIVIDUAL_MASTERY requires SYSTEM. Checkout, diff, reconcile, push, publish, and immutable versions preserve the declaration. Assignment and retraining records remain authoritative for what a worker actually receives.

opentrain lms checkout --course <course-id> --dir ./course
opentrain lms diff --dir ./course --json
opentrain lms pull --dir ./course --json
opentrain lms reconcile --dir ./course --json
opentrain lms validate --dir ./course --remote --key validate-course-v1
opentrain lms push --dir ./course --key push-course-v1

Private assets use prepare → direct-to-storage upload → verified finalize. Videos above one 6 MiB chunk use resumable TUS automatically; the current effective direct-upload video ceiling is reported by lms capabilities (256 MiB at this release). Upload grants are transient secrets and are never printed.

opentrain lms assets upload --course <course-id> --kind VIDEO \
  --file ./walkthrough.mp4 --key walkthrough-video-v1 --json
opentrain lms assets list --course <course-id> --json
opentrain lms assets get --course <course-id> --asset <asset-id> --json

Publishing, job linking, enabling Training, and assigning learners are separate confirmed actions. None silently performs another:

opentrain lms publish --dir ./course --confirm-publish --key publish-course-v1
opentrain lms training link --job <job-id> --course <course-id> \
  --confirm-link --key link-course-v1
opentrain lms training set --job <job-id> --enabled true \
  --confirm-training --key enable-training-v1
opentrain lms assign --job <job-id> --course <course-id> --version <version-id> \
  --audience SELECTED --selected-contracts <contract-id> --policy SYSTEM \
  --confirm-assign --key assign-course-v1

Native Forms assessments can be created, revision-fenced, published, bound to lessons, graded, released, decided, and returned for another attempt. Run opentrain lms assessments help and the focused leaf help for their exact JSON schemas and mutation boundaries.

For individualized retraining, capture the QC evidence in a DRAFT case and run opentrain lms retraining scaffold --case <case-id> --json. The scaffold is strictly no-write: it returns the masked recipient, grounded outline, effective authoring intent, deterministic capture hash, constraints, and the ordered existing LMS/Native Forms/Preview/publish/assignment operations. It creates no course, assessment, Preview, assignment, Project To-do, notification, or attempt. Review and author the content before using the separate signed plan and confirmed assignment commands.

Hosted visual proof renders the real worker player without creating learner progress. Rendering captures the session's current focus (target flags are assertions, not navigation); typed steps use a closed action grammar and return before/after evidence. Human review links are one-time-returned, short-lived, navigation-only, revocable, and require a signed-in employer organization member:

opentrain lms preview render --session <id> --expected-state-revision <n> \
  --viewport desktop --viewport mobile --key <key> --output-dir ./evidence --json
opentrain lms preview step --session <id> --expected-state-revision <n> \
  --action complete_content --viewport desktop --key <key> --json
opentrain lms preview share --session <id> --lesson <lesson-key> \
  --expires-in 900 --key <key> --json
opentrain lms preview links revoke --session <id> --link <link-id> --key <key> --json
opentrain lms preview audit --session <id> --viewports desktop,mobile \
  --out ./complete-worker-audit --key <stable-root-key> --json

PNG, ARIA, diagnostics, and manifest artifacts are revision-bound and verified for checksum, byte size, signature, and dimensions before success is reported. The audit command walks the exact pinned course in canonical order, captures assessment lessons before answers are supplied, supports safe per-target retries, and publishes its local evidence directory only when the complete course succeeds. It never creates learner progress, assignments, Project To-dos, Forms attempts, or notifications. Run each leaf with help for its exact schema and zero-write boundary.

Task Checklists (machine-first)

Adaptive per-job Task Checklist authoring with full web-builder parity (job_checklists:read/write; default-off agent surface). The family is machine-first: exactly one typed JSON envelope on stdout by default, --pretty for humans. File-first workflow — the server owns every product rule:

opentrain checklists init --dir ./checklist --title "Vehicle checks" --required
opentrain checklists validate --dir ./checklist --job-id <job-id>   # no writes
opentrain checklists push --dir ./checklist --job-id <job-id>       # three-way merge + CAS save
opentrain checklists publish --dir ./checklist --confirm-live       # applied impact + sweep counts
opentrain checklists preview --dir ./checklist --open               # secret rides the URL fragment
opentrain checklists submissions --job-id <job-id>
opentrain checklists enable --job-id <job-id> --key <k>             # worker visibility (final step)

Run opentrain checklists help for the full lifecycle, key policy (reversible transitions require an explicit --key), and output contract.

Reviewer Feedback (employer-only, machine-first)

Job-scoped reviewer feedback (job_feedback:read/write; default-off privacy-sensitive agent surface). Subjects never read feedback, author identity, severity, notes, history, or counts through any agent surface. EVERY mutation requires an explicit --key; triage carries the required --expected-updated-at CAS token:

opentrain feedback settings --job-id <job-id>          # works while the toggle is OFF
opentrain feedback enable --job-id <job-id> --confirm --key <k>
opentrain feedback roster --job-id <job-id>            # canonical subject people + contracts
opentrain feedback submit --job-id <job-id> --subject-user-id <user-id> \
  --severity MAJOR --suggested-action RETRAINING \
  --report "Systematically mislabeled the calibration batch." --key <k>
opentrain feedback submit --job-id <job-id> --subject-user-id <user-id> \
  --assessment POSITIVE --overall-performance EXCEEDS_EXPECTATIONS --key <k>
opentrain feedback list --job-id <job-id> --status OPEN --severity MAJOR
opentrain feedback list --job-id <job-id> --lane RECORDED --assessment POSITIVE
opentrain feedback triage --report-id <id> --to-status IN_REVIEW \
  --expected-updated-at <updatedAtIso> --key <k>
opentrain feedback access grant --job-id <job-id> --user-id <user-id> --key <k>

Balanced evaluations (DEV-3168): without --assessment the legacy shape stays required exactly as before (severity + report; recorded as a needs-improvement concern). With --assessment, severity present means a concern is flagged — POSITIVE never takes severity/suggested action, NEEDS_IMPROVEMENT requires severity, and any flagged concern requires the report body. Optional --overall-performance / --knowledge anchored ratings default to Not rated. Non-concern rows form the "Recorded" lane: their stored severity MINOR / status OPEN are compatibility shadows, so --lane RECORDED cannot combine with a concrete --status or --severity (usage error locally; typed 400 on the server). Rating aggregates are distributions/latest/rated-n only — never average, score, or rank them.

Run opentrain feedback help for the full command set (summary, get, link-subject, access revoke/bulk, disable) and the exact note/confirmation rules.

Job drafting & publishing

Draft create is the primary workflow. Send a normal job description (inline or from a file); OpenTrain parses it server-side into structured job fields and returns an unpublished draft URL plus validation state:

cat > /tmp/job.txt <<'EOF'
We need experienced image annotation specialists to draw bounding boxes around
retail products in catalog photos. The dataset has about 1,200 JPG files.
Contributors should have prior image labeling or QA experience, write short
notes for edge cases, and report daily throughput. Pay is $25 per hour.
EOF

opentrain jobs draft create --description-file /tmp/job.txt

When the draft is not publish ready, each missing field in the output includes an ask: question to relay to the human, the value type, allowed enum values, and the field name(s) to set. Fill gaps one field at a time:

opentrain jobs draft update --job-id <id> --set aiInterviewRequirements="Explain your QA process"

Numbers, booleans, and JSON arrays are auto-coerced; everything else stays a string. Repeat until publish ready, then:

opentrain jobs publish --job-id <id>

Publishing runs the same validation + moderation pipeline as the in-app flow and is subject to per-account daily publish limits. Structured source systems can use --canonical-file or --payload-file instead of a plain description.

Proposals, hiring, and messaging

# Rank candidates for a job (bid, status, AI-interview and match-score signals)
opentrain proposals list --job-id <id>

# Full candidate evaluation: bid, AI-interview score + summary, location and
# identity verification, Open Label assessment, and contract state.
# --interview appends the sanitized AI-interview transcript.
opentrain proposals get --proposal-id <id> --interview

# Masked freelancer profile by user id or public profile slug: skills, stats,
# work/label experience, education, reviews, languages. Names stay masked to
# first name + last initial; personal contact details (including personal
# email) are never returned.
opentrain freelancers get --id <user-id-or-slug>

# Read-only invitation state for an employer-owned job (DEV-2846): INVITED,
# DECLINED, APPLIED, or HIRED (hireAcceptancePending separates an
# authorized-but-unaccepted hire). No expiry is ever fabricated. Exactly one
# { ok, data } JSON envelope by default; --pretty for human lines; --all
# follows nextCursor and reports explicit collection metadata.
opentrain jobs invitations list --job-id <id> --state INVITED --all
opentrain jobs invitations get --job-id <id> --freelancer-id <id> --pretty

jobs invitations list / get read canonical Joboffer rows on the proposals:read scope inside the default-off proposal_management family (AGENT_SURFACE_PROPOSAL_MANAGEMENT_ENABLED; a disabled family is a typed 403 AGENT_SURFACE_DISABLED): withheld (hidden, banned, or closed) profiles never appear in a page and look up as invited: false with a null invitation, the same shape as never invited. The --state filter is the exact uppercase enum (INVITED|DECLINED|APPLIED|HIRED); lowercase, unknown, or invented values (such as EXPIRED) are usage errors before any HTTP request.

Rate integrity (DEV-3025): every rate-bearing read (proposals list, proposals get, freelancers get, talent search, dossier get, dossier applications) carries a status next to the number — VALID, MISSING, or INVALID_RATE_DATA (the stored value fell outside the canonical 1..1000 USD / 2-decimal shape, so the number is null rather than corrupt). Human output prints Rate unavailable (INVALID_RATE_DATA) for that case; n/a still means "not provided". JSON output is the exact server projection.

One identity verdict across every read

proposals get, freelancers get, and dossier get return the SAME structured identity verdict for the same candidate at the same time: explicit status (VERIFIED, REQUESTED, IN_PROGRESS, FAILED, EXPIRED, NEEDS_REVIEW, CANCELED, REQUIRED_NOT_REQUESTED, NOT_REQUESTED, UNKNOWN), derived verified/current, provider, country, timestamps, an asOf freshness anchor, source (canonical record vs legacy Stripe), and an explicit conflict object when sources disagree. For hiring and onboarding decisions, the structured verdict (verification.identity / identityVerification) is authoritative; top-level identityVerified booleans are derived compatibility fields and can never contradict it. UNKNOWN means the signal was unavailable — never treat it as "not verified". Proposal-time location, current location, and project eligibility are separately scoped facts, not identity.

Candidate dossier (bounded evidence)

# One-call proposal-linked, viewer-scoped dossier (concise by default)
opentrain dossier get --proposal-id <id>

# Auditable mode: adds ten bounded metadata-only evidence families
opentrain dossier get --proposal-id <id> --evidence full --json

# Audited resume access (DEV-2977 B3.5): fresh 600-second signed URL when
# AVAILABLE, truthful typed states otherwise. Requires candidate_documents:read
# IN ADDITION to candidate_dossier:read; every access is audited append-only.
opentrain dossier resume --proposal-id <id> --json

# Pageable lists behind the bounded previews
opentrain dossier reviews --proposal-id <id> --limit 10
opentrain dossier contract-history --proposal-id <id> --limit 10

# PLATFORM ADMIN ONLY (DEV-2887): one candidate's cross-organization
# application history from canonical Joboffer rows. Exactly one { ok, data }
# JSON envelope by default; --pretty for human lines; --all collects every
# page inside the original asOf snapshot and reports collection metadata.
opentrain dossier applications --user-id <id> --limit 50
opentrain dossier applications --user-id <id> --all --pretty

dossier resume is LINK-ONLY on the agent surface: no mode flag exists, the request pins ?mode=link, redirects are never followed, and no bytes are downloaded. Download the file with your own client from the returned signed URL (curl -L "<url>" -o resume.pdf), or follow the dossier's stable links.resume.url (…/dossier/resume?mode=redirect) which 302s to a fresh signed URL — candidate self-access is refused with a typed SELF_ACCESS_NOT_SUPPORTED denial.

Resume provenance (DEV-3024) travels with the link as five correlated variants. When availability is AVAILABLE, provenanceStatus is either AVAILABLE with a provenance object — storage-observed fileName, contentType, sizeBytes, uploadedAt, lastModifiedAt, an observedAt server clock, a checksum ({ algorithm: "SHA256", value }) only when storage recorded one (null today), and the opaque storageEtag (an HTTP validator, NOT a checksum) — or UNAVAILABLE with provenance: null when the metadata read failed while the signed URL stayed valid. Every unavailable link state carries provenanceStatus: NOT_APPLICABLE and provenance: null. Nothing is parsed from the file and no storage path, bucket, or version identifier is ever returned.

dossier applications is a PLATFORM ADMIN ONLY read behind the same candidate_dossier:read scope: PROPOSAL and INVITE rows are returned and counted SEPARATELY, counts and velocity cover the whole history as of asOf (not only the current page), bid amounts appear only behind the admin gate with a rate-integrity status, and cover letters, contact details, messages, assessments, and private employer review notes are never returned (redaction.excluded). Non-admins and unknown candidates read one uniform non-enumerating 404. Pages are bound to the original asOf, so --all collects a single consistent snapshot.

# Audited participant work email (DEV-3031 B3.6): the managed
# @opentrain.work address for a worker you manage — exactly one selector.
opentrain participant work-email --contract-id <id> --json
opentrain participant work-email --proposal-id <id>
opentrain participant work-email --job-id <id> --person-user-id <id>

participant work-email requires the dedicated participant_work_email:read scope and returns ONLY the OpenTrain-managed address — never the personal signup/login email. The selector grammar is exact by flag presence: a bare user id is never accepted, mixed selector flags fail even with empty values, and every violation is a usage error with zero HTTP. ONLY the ACTIVE state carries an address; ended contracts read as typed CONTRACT_ENDED, un-hired proposals as typed NOT_HIRED; every authenticated attempt is audited append-only server-side BEFORE any address is disclosed.

--evidence is a strict lowercase enum: concise (default — the response's evidenceMode is CONCISE and evidence is null) or full. An empty, uppercase, whitespace-wrapped, unknown, or repeated value — in either the split or --evidence= form — is a deterministic usage error before any HTTP request, never a silent downgrade. Full mode adds ten bounded metadata-only families (identity verification history, identity requests, eligibility decisions, location observations, proposal and contract facts, a contract-history preview, work-assessment attempts, curated reviews, and proposal-DM activity metadata), each capped at 25 items with a normalized totalCount, a truthful truncated flag, and a typed continuation (COMPLETE, BOUNDED_SNAPSHOT_ONLY, or OPERATION + cursor into dossier reviews / dossier contract-history). Evidence and engagement history are viewer-scoped: an employer or delegate read covers the principal employer's own history with the candidate (engagement.scope: "EMPLOYER_CANDIDATE"), while a candidate reading their own dossier sees only the current proposal's job (engagement.scope: "CURRENT_PROPOSAL_JOB"). Evidence never contains message bodies or participants, contact details, raw resume files or URLs, government identity documents, provider/session references, assessment contents, compensation amounts in cross-buyer history, or coordinates/IP/ASN/timezone data. The human summary prints per-family counts; the complete structured evidence block requires --json.

# Hire request: does NOT hire or move money. Records a pending approval and
# returns an approvalUrl a signed-in human must open and confirm in the
# OpenTrain app (~72h expiry). Requires a claimed account + payment method or
# covering credit balance — a 409 response includes a billingUrl for the human.
opentrain proposals hire --proposal-id <id> --amount 500

# Open the pre-hire DM thread for a proposal
opentrain messages start-proposal-thread --proposal-id <id>

# Invite a freelancer to a published job
opentrain jobs invite --job-id <id> --freelancer-id <id>

# Conversations and messages
opentrain messages list --filter all --limit 20
opentrain messages read --conversation-id <id> --limit 50
opentrain messages send --conversation-id <id> --content "Thanks — looks great."

Message reads are read-only: they do not create conversations, mark messages read, or return raw attachment URLs. Sending requires a claimed account.

Talent discovery (search & reputation)

# Hybrid ranked search over the vetted talent pool (semantic + lexical + fuzzy)
opentrain talent search --q "rubric writing finance" --available-now --limit 20 --json

# Facet browse (no --q): reputation-ranked listing filtered by facets only
opentrain talent search --skill accounting --country PH --tier PROVEN

# Aggregate reputation card for one profile
opentrain talent reputation --slug <profile-slug-or-user-id>

With --q, results rank by hybrid relevance (semantic + full-text + fuzzy match over titles, skills, and bios). Without --q, the command is a facet browse ordered by reputation. --query is an alias for --q, and bare positional words are treated as the query.

All filters compose with each other and with --q: --skill, --language, --country <ISO>, --badge VERIFIED_TRAINER, --talent-type Individual|Agency, --availability <text> (freeform facet), --min-reputation <n>, --tier PROVEN|ESTABLISHED|PROVISIONAL (alias --confidence-tier), and --available-now (only people with no current assignment load). Sorting: --sort relevance|reputation|rate_asc|rate_desc|recent — the default is relevance with --q and reputation order without it. Pagination: --limit accepts 1..50 (default 25) plus --offset. Invalid parameter shapes return 400 BAD_REQUEST with the offending parameter named in details rather than being silently coerced.

Results are privacy-masked aggregate talent cards: first name + last initial, no contact details, no documents, no cross-buyer private assessments. Representative --json response:

{
  "results": [
    {
      "card": {
        "userId": "cm…",
        "slug": "ada-l",
        "displayName": "Ada L.",
        "title": "Finance & accounting AI evaluator",
        "countryCode": "DE",
        "hourlyRateUsd": 30,
        "reputation": { "compositeScore": 91.4, "confidenceTier": "ESTABLISHED" },
        "badges": []
      },
      "score": 0.87
    }
  ],
  "total": 1,
  "limit": 20,
  "offset": 0,
  "generatedAt": "2026-08-29T00:00:00.000Z"
}

Contracts and milestones (co-signed money movement)

# Contract status, milestones with funding/approval state, budget consumption,
# and the post-hire job DM conversationId. The global list (no --job-id) is a
# bounded page (DEV-2839): default 50, max 100, newest first, with nextCursor.
opentrain contracts list --status active
opentrain contracts list --status active --limit 100 --cursor <nextCursor>
# Collect every page at a valid page size with truthful collection metadata
# (pagesFetched, collectedCount, complete, finalCursor, stopReason). --all with
# --job-id pages the job-scoped read too; without --limit/--cursor/--all a
# job-scoped read keeps the legacy unpaged list.
opentrain contracts list --status active --all --json
opentrain contracts get --contract-id <id>

# Add an unfunded milestone directly (no money moves)
opentrain milestones create --contract-id <id> --description "Batch 2: 5,000 labels" --amount 250

# Funding and approval record a pending approval and return an approvalUrl —
# a signed-in human confirms in the OpenTrain app before money is held or released
opentrain milestones fund --milestone-id <id>
opentrain milestones approve --milestone-id <id>
opentrain approvals get --approval-id <id>

# Ending a contract succeeds only after every milestone is completed,
# cancelled, or successfully refunded; 409 responses list blockers and actions
opentrain contracts end --contract-id <id>

Updates feed and webhooks

# Cursor-based delta feed: new proposals, messages, contract/milestone
# changes, approval confirmations, budget state changes. Persist the returned
# nextCursor and pass it on the next poll.
opentrain updates poll --cursor <nextCursor>

# Or push: signed HMAC webhook deliveries of the same events. The signing
# secret is shown once at creation.
opentrain webhooks create --url https://example.com/hooks/opentrain --events proposal.created,approval.confirmed
opentrain webhooks list

Credits (prepaid balance)

opentrain credits show
opentrain credits ledger

# Returns a Stripe Checkout URL a human completes — the agent cannot pay
opentrain credits top-up --amount 500
opentrain credits top-up-status --top-up-id <id>

Team and payments

opentrain team show
opentrain team invite --email [email protected]
opentrain payments pending
opentrain payments verify --invoice-id <invoice-id>
opentrain payments recovery-preflight --invoice-id <invoice-id> [--approval-id <approval-id>]
opentrain tokens list

payments pending and payments verify are read-only. payments verify retrieves current provider state and reports reversals or disagreement with OpenTrain. payments recovery-preflight additionally searches every matching Stripe transfer page and refuses to call a manual replacement safe when an unreversed equivalent already exists. None of these commands can fund, approve, refund, release, reconcile, or move money.

Configuration

  • Saved config: ~/.config/opentrain/cli.json (or $XDG_CONFIG_HOME/opentrain/cli.json), shared with the OpenTrain MCP server (@opentrain-ai/mcp) — registering through either surface makes the credentials available to both.
  • Env overrides: OT_API_TOKEN / OPENTRAIN_API_TOKEN and OT_API_BASE_URL / OPENTRAIN_API_BASE_URL.
  • Default base URL: https://app.opentrain.ai.

Reliability

Requests time out after 30 seconds (publish and hire get 120 seconds). The CLI automatically retries transient failures with exponential backoff (up to 3 attempts): reads (GET) retry on 502/503/504 gateway errors and network failures; writes retry only on errors that occur before the request reaches the server (connection refused, DNS failures), so a write is never sent twice.

Exit codes and JSON error envelope

Every non-2xx API response exits nonzero; automation must check the exit status, not the presence of output. When an OpenTrain API request fails and --json was passed, the CLI prints exactly one JSON envelope on stdout (instead of the human-readable stderr lines) and still exits 1:

{
  "ok": false,
  "error": {
    "code": "INTERNAL_ERROR",
    "status": 500,
    "message": "Failed to search talent",
    "requestId": "req_abc123",
    "details": { "reason": "..." }
  }
}

code is always present: the server's machine code when it returned an error envelope, or the stable fallback HTTP_ERROR when it did not. requestId is taken from the error envelope or the x-request-id response header and is null only when neither provided one. details is present only when the server returned structured details. The envelope carries server-provided fields only — never tokens, headers, or stack traces.

Transient failures and concurrency

When a platform dependency behind authentication is briefly unavailable, the API returns a retryable 503 AUTHORIZATION_UNAVAILABLE envelope with a Retry-After header and a requestId instead of a terminal error. The CLI's standard retry (3 attempts on 502/503/504 for GET/HEAD reads, exponential backoff) absorbs these automatically. The write-safety boundary is exact: writes retry only on pre-connection failures (connection refused, DNS) where the request provably never reached the server — a write HTTP response or an ambiguous mid-flight failure is never retried, so a write is never sent twice. Terminal 401/403 responses are attempted exactly once. Modest concurrent read batches (for example four parallel talent search calls) are a supported envelope; each request retries independently. Quote the requestId when reporting a failure so operators can find the exact server-side request.

Development (monorepo)

npm install
npm run build -w @opentrain-ai/cli
npm exec -w @opentrain-ai/cli opentrain -- whoami