---
name: pp-milanote
description: "Treat a Milanote board like an addressable canvas: native headings, lists and checkboxes, laid out on an even grid, and read back as Markdown. Trigger phrases: `put this on my Milanote board`, `lay out these notes in Milanote`, `read my Milanote board`, `what's left on my Milanote checklist`, `use milanote`, `run milanote-pp-cli`."
author: "Beau Sterling"
license: "Apache-2.0"
argument-hint: "<command> [args] | install cli|mcp"
allowed-tools: "Read Bash"
metadata:
  openclaw:
    requires:
      bins:
        - milanote-pp-cli
    install:
      - kind: go
        bins: [milanote-pp-cli]
        module: github.com/mvanhorn/printing-press-library/library/productivity/milanote/cmd/milanote-pp-cli
---
<!-- GENERATED FILE — DO NOT EDIT.
     This file is a verbatim mirror of library/productivity/milanote/SKILL.md,
     regenerated post-merge by tools/generate-skills/. Hand-edits here are
     silently overwritten on the next regen. Edit the library/ source instead.
     See the repository agent guide, section "Generated artifacts: registry.json, cli-skills/". -->

# Milanote — Printing Press CLI

## Prerequisites: Install the CLI

This skill drives the `milanote-pp-cli` binary. **You must verify the CLI is installed before invoking any command from this skill.** If it is missing, install it first:

1. Install via the Printing Press installer. It defaults binaries to `$HOME/.local/bin` on macOS/Linux and `%LOCALAPPDATA%\Programs\PrintingPress\bin` on Windows:
   ```bash
   npx -y @mvanhorn/printing-press-library install milanote --cli-only
   ```
2. Verify: `milanote-pp-cli --version`
3. Ensure the reported install directory is on `$PATH` for the agent/runtime that will invoke this skill.

If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.10 or newer). This installs into `$GOPATH/bin` (default `$HOME/go/bin`), so add that directory to `$PATH` instead:

```bash
go install github.com/mvanhorn/printing-press-library/library/productivity/milanote/cmd/milanote-pp-cli@latest
```

If `--version` reports "command not found" after install, the runtime cannot see the binary directory on `$PATH`. Do not proceed with skill commands until verification succeeds.

Every Milanote board, note, heading, to-do, image and comment from the command line, written through the same channel the web app uses. layout md turns a Markdown file into an evenly spaced board, outline turns a board back into Markdown, and a local mirror makes search and todos open work across every board offline.

## When to Use This CLI

Use this CLI when an agent needs to read, build or reorganize Milanote boards: pushing a Markdown outline onto a board as headings, notes and checklists, tidying a board onto an even grid, uploading a folder of storyboard frames, or reading a board tree back as text.

## Anti-triggers

Do not use this CLI for:
- Drawing sketches or editing images inside Milanote
- Real-time collaboration or watching other people's cursors
- Accounts you are not logged in to in a browser

## Unique Capabilities

### Canvas layout
- **`layout md`** — Turn a Markdown file into native heading cards, rich-text notes, bullet lists and checkbox lists in evenly spaced columns.

  _Reach for this whenever a multi-section document has to land on a board looking organized, not one card at a time._

  ```bash
  milanote-pp-cli layout md outline.md --board 1Ab2Cd3EfGh4iJ --cols 3 --gutter 4
  ```
- **`layout grid`** — Re-space a board's cards (or empty its Unsorted panel) onto an even grid with equal gutters, and undo it if you don't like it.

  _Use it after edits to keep a board tidy without moving cards one by one._

  ```bash
  milanote-pp-cli layout grid 1Ab2Cd3EfGh4iJ --cols 4 --gutter 4 --dry-run
  ```
- **`images grid`** — Upload a folder of frames as an evenly spaced, numbered grid with optional captions.

  _Use it for storyboards and reference boards instead of dropping images one at a time._

  ```bash
  milanote-pp-cli images grid ./frames --board 1Ab2Cd3EfGh4iJ --cols 3 --number
  ```
- **`layout check`** — Report off-grid cards, uneven gutters, overlaps, cards stuck in Unsorted, and text that should be a heading or list.

  _Use it to verify a layout by numbers instead of a screenshot._

  ```bash
  milanote-pp-cli layout check 1Ab2Cd3EfGh4iJ --json
  ```
- **`mirror`** — Pair a Markdown file with a board so edits on either side flow to the other.

  _Use it when a document lives in both an editor and a board and must stay in step._

  ```bash
  milanote-pp-cli mirror script.md --board 1Ab2Cd3EfGh4iJ --once
  ```
- **`template apply`** — Save a board's structure as a small YAML kit and stamp out fresh copies.

  _Use it to start every project from the same organized layout._

  ```bash
  milanote-pp-cli template apply film-kit.yaml --parent 1Ab2Cd3EfGh4iJ --name 'New short' --dry-run
  ```
- **`layout csv`** — Turn spreadsheet rows such as a shot list into a numbered, evenly spaced grid of cards.

  _Use it for tabular data instead of building cards one by one._

  ```bash
  milanote-pp-cli layout csv shots.csv --board 1Ab2Cd3EfGh4iJ --title-col shot --cols 4
  ```

### Read it back
- **`outline`** — Get a board and its sub-boards back as Markdown in canvas reading order, with headings, lists and checkboxes intact.

  _Use it to read a whole board as text before editing or summarizing it._

  ```bash
  milanote-pp-cli outline 1Ab2Cd3EfGh4iJ --depth 2 --out board.md
  ```
- **`todos open`** — List every unchecked checkbox across your boards with where it lives, ready to tick off.

  _Use it to answer what is left to do without opening every board._

  ```bash
  milanote-pp-cli todos open --agent
  ```
- **`snapshot diff`** — Save a board's state and later see exactly which cards were added, moved, edited or ticked.

  _Use it to review what changed between revisions without eyeballing the canvas._

  ```bash
  milanote-pp-cli snapshot diff 1Ab2Cd3EfGh4iJ --json
  ```

### Agent-native plumbing
- **`chrome-login`** — Pull the Milanote login from a running Chrome in one step.

  _Use it to set up or refresh auth without exporting cookies by hand._

  ```bash
  milanote-pp-cli chrome-login --port 9222
  ```

## Discovery Signals

This CLI was generated with browser-observed traffic context.
- Capture coverage: 20 API entries from 20 total network entries
- Protocols: rest_json (75% confidence)
- Candidate command ideas: create_presign — Derived from observed POST /api/elements/attachments/presign traffic.; list_boards — Derived from observed GET /api/boards traffic.; list_counts — Derived from observed GET /api/users/me/counts traffic.; list_custom_templates_init — Derived from observed GET /api/users/me/custom-templates-init traffic.; list_history — Derived from observed GET /api/users/me/history traffic.; list_icons — Derived from observed GET /api//icons traffic.; list_labels — Derived from observed GET /api/labels traffic.

`--ca-cert`, `MILANOTE_CA_CERT`, or `ca_cert` adds a PEM CA bundle to the system trust roots. `--tls-server-name`, `MILANOTE_TLS_SERVER_NAME`, or `tls_server_name` is the name checked against the certificate. Precedence is flag, then environment, then the config file. A config file other users can change does not supply these settings. `doctor` reports the trust in use and names the setting that fixes a chain or hostname failure.

## Command Reference

**account** — Your Milanote account

- `milanote-pp-cli account counts` — Card and board counts used against plan limits
- `milanote-pp-cli account history` — Recently viewed boards (raw history ids and timestamps)
- `milanote-pp-cli account me` — Show the signed-in user, root board id and plan
- `milanote-pp-cli account trash` — Elements in your trash
- `milanote-pp-cli account users` — Look up collaborators by user id

**boards** — Boards: the canvases that hold notes, headings, images and sub-boards

- `milanote-pp-cli boards fetch` — Fetch raw board payloads (elements, comments, labels) by id
- `milanote-pp-cli boards modified` — Last-modified times for boards
- `milanote-pp-cli boards permissions` — Who can see and edit a board
- `milanote-pp-cli boards search` — Search board titles on the server

**labels** — Card labels

- `milanote-pp-cli labels` — List your labels


**auth**


- `milanote-pp-cli chrome-login` — Pull the Milanote login from a running Chrome in one step.


**images**


- `milanote-pp-cli images grid` — Upload a folder of frames as an evenly spaced, numbered grid with optional captions.


**layout**


- `milanote-pp-cli layout md` — Turn a Markdown file into native heading cards, rich-text notes, bullet lists and checkbox lists in evenly spaced columns.

- `milanote-pp-cli layout grid` — Re-space a board's cards (or empty its Unsorted panel) onto an even grid with equal gutters, and undo it if you don't like it.

- `milanote-pp-cli layout check` — Report off-grid cards, uneven gutters, overlaps, cards stuck in Unsorted, and text that should be a heading or list.

- `milanote-pp-cli layout csv` — Turn spreadsheet rows such as a shot list into a numbered, evenly spaced grid of cards.


**mirror**


- `milanote-pp-cli mirror` — Pair a Markdown file with a board so edits on either side flow to the other.


**outline**


- `milanote-pp-cli outline` — Get a board and its sub-boards back as Markdown in canvas reading order, with headings, lists and checkboxes intact.


**snapshot**


- `milanote-pp-cli snapshot diff` — Save a board's state and later see exactly which cards were added, moved, edited or ticked.


**template**


- `milanote-pp-cli template apply` — Save a board's structure as a small YAML kit and stamp out fresh copies.


**todos**


- `milanote-pp-cli todos open` — List every unchecked checkbox across your boards with where it lives, ready to tick off.




### Finding the right command

When you know what you want to do but not which command does it, ask the CLI directly:

```bash
milanote-pp-cli which "<capability in your own words>"
```

`which` resolves a natural-language capability query to the best matching command from this CLI's curated feature index. Exit code `0` means at least one match; exit code `2` means no confident match — fall back to `--help` or use a narrower query. `--json` (and other machine formats) keep that exit-2 contract and write `{"matches":[]}` on stdout so agents can inspect the envelope without treating a miss as success.

## Recipes

### Push a scene outline

```bash
milanote-pp-cli layout md scenes.md --board 1Ab2Cd3EfGh4iJ --cols 3
```

Each ## becomes a heading column with its notes and checklists underneath, evenly spaced.

### Read a board back

```bash
milanote-pp-cli outline 1Ab2Cd3EfGh4iJ --out board.md
```

Markdown in reading order, checkboxes preserved.

### Just the card texts

```bash
milanote-pp-cli boards get 1Ab2Cd3EfGh4iJ --agent --select id,type,text
```

Narrow a large board payload to ids, types and plain text.

### What's left to do

```bash
milanote-pp-cli todos open --agent
```

Every unchecked task across boards with its board path and id.

## Auth Setup

Milanote has no API keys. The CLI uses the session cookie from a browser where you are logged in. Easiest: start Chrome with remote debugging and run chrome-login, which copies only the Milanote session cookie. Or paste it with auth login --cookie, or point MILANOTE_COOKIE_FILE at a JSON cookie export. Sessions last about a month; doctor tells you when it has expired.

Run `milanote-pp-cli doctor` to verify setup.

## Agent Mode

Add `--agent` to any command. Expands to: `--json --compact --no-input --no-color`.

Global format flags share one contract on promoted, novel, sync, and `--deliver` paths:

- `--json` — one JSON document on stdout (sync progress events go to stderr)
- `--compact` — keep identity/status/timestamp fields; does not change the document vs stream shape
- `--csv` / `--plain` — tabular rows (collection envelopes unwrap to the row array). `--csv` prefixes `'` on a cell or header key that starts with `=`, `+`, `-`, `@`, tab, CR, or LF (fullwidth `＝＋－＠` too) so a spreadsheet does not run it as a formula; a plain signed number such as `-5` or `+2.5e3` is unchanged.
- `--quiet` — one identity value per row, no envelope

- **Pipeable** — JSON on stdout, errors on stderr
- **Filterable** — `--select` keeps a subset of fields. Dotted paths descend into nested structures; arrays traverse element-wise. Critical for keeping context small on verbose APIs:

  ```bash
  milanote-pp-cli boards search --query example-value --agent
  ```
- **Previewable** — `--dry-run` shows the request without sending. A completed dry run exits 0 with `"success": true` and `"dry_run": true` and no HTTP `status`; validation failures still exit non-zero with `"success": false`.
- **Offline-friendly** — sync/search commands can use the local SQLite store when available
- **Non-interactive** — never prompts, every input is a flag
- **Read-only** — do not use this CLI for create, update, delete, publish, comment, upvote, invite, order, send, or other mutating requests

### Response envelope

Commands that read from the local store or the API wrap output in a provenance envelope:

```json
{
  "meta": {"source": "live" | "local", "synced_at": "...", "reason": "..."},
  "results": <data>
}
```

Parse `.results` for data and `.meta.source` to know whether it's live or local. A human-readable `N results (live)` summary is printed to stderr only when stdout is a terminal AND no machine-format flag (`--json`, `--csv`, `--compact`, `--quiet`, `--plain`, `--select`) is set — piped/agent consumers and explicit-format runs get pure JSON on stdout.

## Paths and state

Agents should treat the CLI's path resolver as part of the runtime contract:

- Use `--home <dir>` for one invocation, or set `MILANOTE_HOME=<dir>` to relocate all four path kinds under one root.
- Use per-kind env vars only when a specific kind must diverge: `MILANOTE_CONFIG_DIR`, `MILANOTE_DATA_DIR`, `MILANOTE_STATE_DIR`, `MILANOTE_CACHE_DIR`.
- Resolution order is per-kind env var, `--home`, `MILANOTE_HOME`, XDG (`XDG_CONFIG_HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME`, `XDG_CACHE_HOME`), then platform defaults.
- `config` contains settings like `config.toml` and profiles. `data` contains `credentials.toml`, `data.db`, cookies, and auth sidecars. `state` contains persisted queries, jobs, and `teach.log`. `cache` contains regenerable HTTP/cache files.
- Stored secrets live in `credentials.toml` under the data dir. Existing legacy `config.toml` secrets are read for compatibility and leave `config.toml` on the first auth write.
- Run `milanote-pp-cli doctor --fail-on warn` to surface path and credential-location warnings. `agent-context` exposes a schema v4 `paths` block for agents that need the resolved dirs.
- For MCP, pass relocation through the MCP host config. The MCP binary does not inherit CLI flags:

  ```json
  {
    "mcpServers": {
      "milanote": {
        "command": "milanote-pp-mcp",
        "env": {
          "MILANOTE_HOME": "/srv/milanote"
        }
      }
    }
  }
  ```

Fleet precedence: an inherited per-kind env var overrides an explicit `--home` for that kind. Use `MILANOTE_HOME` or per-kind vars as durable fleet levers, and use `--home` only for a single invocation. Relocation is not reversible by unsetting env vars; move files manually before clearing `MILANOTE_HOME`, or `doctor` will not find credentials left under the former root.

Local data is unencrypted and relies on OS account permissions plus disk encryption. Reads in the default `--data-source auto` mode are written to the SQLite store; GET responses are cached for five minutes under the cache dir. `local-data purge` previews what it would erase (store files, response cache, learn journal) and deletes only with `--yes`; it is not exposed over MCP. `MILANOTE_NO_LOCAL_DATA=1` disables the response cache and store writes, turns `auto` reads into live reads, and blocks `sync` and `workflow archive`; `--data-source local` still reads an existing store, and `MILANOTE_DATA_SOURCE` sets the `--data-source` default.

Layout commands keep small JSON files under the state directory (`~/.local/state/milanote-pp-cli` unless `XDG_STATE_HOME` is set): `snapshots/` holds saved copies of board cards, `mirror/` remembers which card came from which part of a file, and `undo/` holds the positions `layout grid --undo` restores. `local-data purge` does not remove them; delete that directory to clear them. The layout, outline, to-do and snapshot commands read boards live rather than from the SQLite store.

## Automatic learning

This CLI ships a self-capturing learning loop. The CLI does its own bookkeeping: every invocation is journaled locally, a failed flag followed by a corrected retry auto-derives a `flag_alias` candidate, and a `teach` on a query family without a playbook auto-synthesizes a `playbook_candidate` from the session's journal. Your job is judgment only: `recall` first, act on surfaced candidates, `teach` the final answer, `playbook amend` when you observe a correction. You never record failures by hand.

### Step 1: `recall` before any discovery

Before list/search/drill commands on a new user question, pass the question as an argv or MCP tool argument to `recall --agent`. Do not interpolate user-controlled text into a shell command line.

Quoted `recall "<question>"` breaks on an apostrophe, which is ordinary English. A quoted heredoc breaks when a body line equals the delimiter, and that delimiter is published in these docs. Write the question with a non-shell file-writing tool, then read it back as data:

```bash
# Write the question verbatim with your file-writing tool (no shell involved).
# Command substitution on a file only ever yields data — the shell never
# parses the file's bytes as syntax.
QUERY=$(cat /path/to/question.txt)
milanote-pp-cli recall "$QUERY" --agent
```

Prefer MCP: pass the question as the tool's query argument. `"$QUERY"` after a file read is argv-safe; putting the question itself in the command text is not.

The response envelope:

```json
{
  "query": "...",
  "normalized": "<normalized form>",
  "query_entities": ["..."],
  "found": true | false,
  "match_score": 0.0,
  "results": [
    { "resource_id": "...", "resource_type": "...", "venue": "...",
      "confidence": 2, "entity_match": "exact|partial|unknown",
      "source": "taught|preseed|pattern", "warnings": ["..."] }
  ],
  "mismatches": [ /* only when --debug-mismatches */ ],
  "warnings": [ /* top-level */ ],
  "candidates": [
    { "id": 12, "class": "flag_alias | playbook_candidate",
      "summary": "...", "sightings": 3, "last_seen": "...",
      "rationale": "...",
      "next_action": ["<trial command>", "milanote-pp-cli learnings confirm 12"] }
  ],
  "playbook": {
    "query_family": "...",
    "playbook": {
      "steps": [ { "cmd": "<command with {slot} substitution>", "purpose": "..." } ],
      "entity_slots": ["$ENTITY"],
      "expected_tool_calls": 3
    },
    "slots_resolved": { "$ENTITY": { "token": "<live token>", "canonical": "<canonical>" } },
    "notes": "<workarounds + gotchas for this query family>"
  },
  "notes": "<duplicate surface for non-playbook callers>"
}
```

Empty-store short-circuit: if the store has no learnings, playbooks, or candidates yet (recall finds nothing and `learnings list` and `learnings candidates` are both empty), skip recall for the rest of this session instead of taxing every query; resume recall-first once something has been taught.

### Step 2: decision tree

Read `candidates`, `playbook`, `notes`, `results[0]`, and warnings in that order:

```
if Candidates present (warnings include "candidates_present"):
    -> candidates are try-then-confirm, never facts. Follow each candidate's
       two-step next_action verbatim: run the trial command first, then run
       `learnings confirm <id>` only after the t