Skip to content

Repository files navigation

pst-md

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 login

CLI

npx 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 whoami

Run npx pst-md help for the full command surface (save/unsave/move, folder rename/delete, consume, appearance, …).

Sign-in and token storage

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:

  1. the PST_MD_TOKEN environment variable (for CI/automation);
  2. the OS keychain (via @napi-rs/keyring);
  3. only with an explicit --insecure-storage flag, a 0600 file 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.

Library

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.

Device sign-in from code

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 approved

Notes are plaintext — private ≠ encrypted

Notes 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.

MCP

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

License

MIT

About

CLI + JS client for pst.md — publish public markdown notes with instant shareable links (npx pst-md)

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages