@pylink/cli
v0.1.3
Published
CLI for controlling a PyLink workspace from the shell. Designed for AI agents: --json output, stable exit codes, stdin piping.
Readme
@pylink/cli
Control a PyLink workspace from the
shell. Designed for AI agents as much as humans: every command accepts
--json for stable machine output, exit codes map to specific failure
classes, and write can read content from stdin.
Install
npm install -g @pylink/cliRequires Node ≥ 20.
Quickstart
# One-time login via OAuth 2.1 device flow. Stores tokens in
# ~/.pylink/config.json (mode 0600).
pylink auth login --server https://pylink-dev.up.railway.app
# Who am I?
pylink whoami
# List, read, write, delete files in the personal workspace.
pylink ls
pylink cat README.md
echo "draft" | pylink write notes/draft.md --stdin
pylink rm notes/draft.mdCommands
Auth
| Command | Description |
|---|---|
| pylink auth login [--server URL] [--force] | RFC 8628 device flow. Emits a verification URL + user code; approve in your browser. |
| pylink auth status | Show logged-in user, server, token expiry. Exits 1 if not logged in. |
| pylink auth logout | Revoke refresh token, delete local config. |
Inspection
| Command | Description |
|---|---|
| pylink whoami | Bootstrap context: user, workspace index, base URL. |
| pylink users | List all users (for sharing). |
| pylink shared-with-me | Files shared with you by others. |
Read / write
All accept --workspace <name> (default personal).
| Command | Description |
|---|---|
| pylink ls | List files in the workspace. |
| pylink cat <path> | Read file contents; emits ETag alongside the body. |
| pylink write <path> (--content <str> \| --stdin) | Create or update. Use --stdin to pipe large / pre-formatted content. |
| pylink write <path> --content X --if-none-match '*' | Conditional create: fails with exit 3 if the file already exists. |
| pylink write <path> --content X --if-match <etag> | Conditional update: fails with exit 3 if the file has changed since you read it. |
| pylink rm <path> [--if-match <etag>] | Delete (optionally conditional). |
| pylink edit <path> --replace <anchor> --with <text> | Anchor-based replace. Fails if the anchor doesn't occur exactly once. |
| pylink edit <path> --insert-after <anchor> --text <text> | Insert immediately after the matched anchor. |
| pylink edit <path> --insert-before <anchor> --text <text> | Insert immediately before the matched anchor. |
Sharing
| Command | Description |
|---|---|
| pylink shares <path> | List who has access to a file. |
| pylink share <path> --user <id> --permission viewer\|editor | Grant access to a user. |
| pylink unshare <path> --user <id> | Revoke a user's access. |
Bulk local sync (for human editor workflows)
| Command | Description |
|---|---|
| pylink pull [<dir>] [--workspace <name>] [--force] | Fetch the whole workspace to a local directory; writes a .pylink/state.json with SHA256 + ETag per file. |
| pylink status [<dir>] | Show A/M/D changes vs the last pull. Exits 1 if dirty. |
| pylink push [<dir>] [--dry-run] | Upload local changes with If-Match / If-None-Match guards. Aborts on any conflict. |
Pull / push honour .pylink/ignore (gitignore-style) so you can exclude
drafts, secrets, etc.
Agent mode: --json
Pass --json on any command to flip both stdout and stderr into stable
machine output:
- stdout: the command's result serialised as pretty JSON (arrays for
list commands, row objects for reads/writes, event objects for multi-step
flows like
auth login). - stderr: a single-object error envelope when the command fails — never any trailing help text — so your parser sees exactly one JSON object.
$ pylink --json whoami
{
"user": { "id": 2, "first_name": "Haoran", "last_name": "Shu" },
"workspace_index": { "personal": 2 },
"base_url": "https://pylink-dev.up.railway.app"
}
$ pylink --json cat missing.md
{
"error": {
"code": "api_error",
"message": "File not found",
"status": 404
}
}
$ echo $?
4auth login --json emits two events on stdout, one per line:
{"event":"device_code","verification_uri":"…","verification_uri_complete":"…","user_code":"ABCD-EFGH","expires_in":600,"interval":5}
{"event":"authenticated","user":{…},"server":"…"}Agents can surface the first line to the human, then wait for the second.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Generic failure (network, unknown server error) |
| 2 | Usage error (missing or conflicting flags) |
| 3 | Precondition failed (412 — stale --if-match or --if-none-match conflict) |
| 4 | Not found (404) |
| 5 | Auth required (401 / 403) — likely pylink auth login needed |
pylink auth status also exits 1 when logged out so scripts can gate on it.
Environment variables
| Var | Effect |
|---|---|
| PYLINK_TOKEN | Bearer token used for every request. Bypasses ~/.pylink/config.json entirely — no login required. Mint one in the web UI under Apps & Integrations → API keys. |
| PYLINK_SERVER | Base URL. When PYLINK_TOKEN is set, this is how the CLI knows where to send requests. Otherwise falls back to the stored config's server, then http://localhost:3000. |
| PYLINK_CONFIG | Path to the config file. Defaults to ~/.pylink/config.json. Useful for CI or multi-account workflows. |
Using @pylink/cli from an AI agent
Agents can't click through the device-flow browser approval, so device flow is a non-starter for headless usage. Use an API key instead:
# 1. In the web UI: Apps & Integrations → API keys → Create.
# Copy the "plk_..." token — it's shown only once.
# 2. Anywhere (CI, sandbox, container), inject it via env:
export PYLINK_TOKEN=plk_...
export PYLINK_SERVER=https://pylink-dev.up.railway.app
# 3. The CLI now works without any config file or login step:
pylink --json whoami
pylink --json ls
echo "draft" | pylink --json write notes/agent.md --stdinPrecedence: if both PYLINK_TOKEN and ~/.pylink/config.json
exist, the env var wins. This means an agent session on a
already-logged-in laptop will use the env token, not your OAuth session.
Security note: env vars are visible to other processes running as
the same user (ps e, /proc/<pid>/environ). API keys are long-lived
by default, so treat them like passwords: store in a secret manager,
rotate via the UI (revoke + recreate) when a host is compromised.
Management endpoints (POST/DELETE /api/me/api-keys) require a web
session, so a leaked key can't self-replace — only a human with
browser access can mint more.
Development
git clone https://github.com/MercuryCod/pylink
cd pylink/cli
npm install
npm run dev -- --help # run via tsx, no build step
npm run build # compile to dist/
npm test # vitestLinks
- Repository: https://github.com/MercuryCod/pylink
- Issues: https://github.com/MercuryCod/pylink/issues
- PyLink design doc:
docs/mvp-spec.mdin the monorepo - MCP spec: https://modelcontextprotocol.io
License
MIT
