CLI + tiny client for pst.md — sign in with a passkey/email account, then publish, edit, delete and organize public or private markdown notes with instant shareable links.
The library is dependency-free and works anywhere fetch exists (Node ≥ 20,
browsers, edge runtimes). The CLI adds account sign-in and stores your token in
the OS keychain.
npm install pst-md # or: npx pst-md loginnpx pst-md login # device-code sign-in (opens a browser)
echo "# hello" | npx pst-md publish # -> url + id + version
npx pst-md publish notes.md --private # owner-only note
npx pst-md get <id> # metadata incl. version
npx pst-md raw <id> # verbatim markdown source
npx pst-md update <id> notes.md # optimistic-concurrency edit (If-Match)
npx pst-md delete <id>
npx pst-md list --filter owned # your library
npx pst-md folder new "Drafts" --color blue
npx pst-md whoamiRun npx pst-md help for the full command surface (save/unsave/move, folder
rename/delete, consume, appearance, …).
login uses the OAuth 2.0 device authorization grant (RFC 8628): it prints
a short code and a URL, opens the URL in your browser (falling back to the
printed URL when headless), then polls until you approve. This works over SSH
and in containers.
The token it stores is a full-authority account session token — treat it like a password. It is resolved in this order:
- the
PST_MD_TOKENenvironment variable (for CI/automation); - the OS keychain (via
@napi-rs/keyring); - only with an explicit
--insecure-storageflag, a0600file under${XDG_CONFIG_HOME:-~/.config}/pst-md/token.
A token is never accepted as a command-line argument (argv is world-readable).
If the keychain is unavailable and you did not pass --insecure-storage, login
fails loudly rather than silently writing a plaintext file. logout forgets it.
import { createClient } from "pst-md";
const pst = createClient({ token: process.env.PST_MD_TOKEN });
// Publish — title/theme via YAML front matter (see https://pst.md/skill.md)
const { note, url, version } = await pst.create(`---
title: Release notes
palette: dracula
---
# v1.0 is out 🎉
`);
console.log(url); // https://pst.md/n/<id>
// Read (public reads need no token)
await pst.get(note.id); // metadata: { id, title, visibility, version, … }
await pst.content(note.id); // verbatim markdown source
// Edit with optimistic concurrency — pass the version you last saw
await pst.update(note.id, { content: "# edited" }, version);
await pst.delete(note.id);
// Library & folders
await pst.list({ filter: "owned" });
await pst.save("someones-public-id");
const folder = await pst.createFolder({ name: "Drafts" });
await pst.move(note.id, folder.id);Errors throw PstError with .status (401 not signed in, 403 not the
owner, 404 unknown/private, 410 deleted, 412 version conflict, 428
missing If-Match, 429 rate-limited) and the server message.
import { createClient, pollDeviceToken } from "pst-md";
const pst = createClient();
const code = await pst.requestDeviceCode();
console.log(`Visit ${code.verification_uri} and enter ${code.user_code}`);
const token = await pollDeviceToken(pst, code); // resolves once approvedNotes are stored as plaintext markdown. There is no client-side encryption.
A --private note is protected by your account (only you can read it), not by
cryptography — never publish secrets you would not hand to the server. A public
note is readable by anyone with the link. Limit: 100 KB per note.
pst.md is also a native MCP server — point
any MCP client at https://pst.md/api/mcp. Agent guide: https://pst.md/skill.md
MIT