clinote

module
v0.1.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 11, 2026 License: MIT

README

clinote

A personal lab notebook for shell commands. One markdown file = one notebook. A persistent shell session is bound to the notebook for the lifetime of the server process — cd, env vars, and shell functions flow between cells. Commands run from fenced code cells; outputs are captured back into the same markdown file as adjacent fenced blocks.

The on-disk file stays plain CommonMark — readable, grep-able, GitHub-renderable. Parsing then re-serialising a notebook without edits produces byte-identical output.

For a tour from install to power-user patterns, read the user guide.

Install

Homebrew (macOS / Linux)
brew tap pmuston/tap
brew trust pmuston/tap      # required for third-party taps
brew install pmuston/tap/clinote

Recent Homebrew refuses to install from an untrusted third-party tap, and the error it prints doesn't make the fix obvious — hence the brew trust line.

Upgrade later with brew upgrade pmuston/tap/clinote.

From source (Go)
go install github.com/pmuston/clinote/cmd/clinote@latest

Requires Go 1.25+. Use this if you don't have Homebrew or want to track main.

Usage

clinote path/to/notebook.md         # open a notebook
clinote new path/to/notebook.md     # create a notebook (with starter content) and open it
clinote                             # list .md files in cwd
clinote --no-browser notes.md       # don't auto-open the browser

The server binds to 127.0.0.1 on a free port and prints the URL to stdout. The browser opens automatically unless BROWSER is empty / none / false / 0, or --no-browser is passed.

clinote new refuses to overwrite an existing file. The title in the scaffolded front matter is derived from the filename (e.g. disk-usage.mdDisk usage).

File format

A notebook is a UTF-8 markdown file with optional YAML front matter and any mix of prose, command cells, and output cells.

Front matter
---
title: Disk usage investigation
created: 2026-05-26T14:30:00Z
shell: bash
---

Recognised fields:

  • title — string
  • created — RFC 3339 timestamp
  • shellbash or zsh (default bash)
  • editabletrue unlocks in-browser editing of sh command bodies (default false)
  • widthfull to use the full window width for the notebook column (default is a narrow column suitable for prose reading)
  • requires — list of environment variable names the notebook needs; a banner names any that are unset. Reports only, never blocks, and cannot execute anything

Unknown fields are preserved on save.

Never export a credential in a cell — cell bodies are written to the .md verbatim. Export it in your shell before launching clinote; the runner inherits that environment. See the user guide.

Command cells

Language tag sh. The body is sent verbatim to the persistent shell.

```sh out=csv
psql -c "select * from users" --csv
```

out=text|csv|jsonl hints the renderer; if absent, the output type is sniffed.

Output cells

Written by the tool, language tag output. Required attributes: type, exit, ran, dur. Optional: truncated=true.

```output type=text exit=0 ran=2026-05-26T14:31:12Z dur=120ms
4.0K    /var/games
2.1G    /var/log
```
Pairing

An output block is paired with the command above it iff only whitespace separates them. Any intervening prose orphans the output and marks the command as unrun. There are no IDs or cross-references — pairing is strictly positional.

Rendering

  • text — wrapped in <pre>; ANSI SGR escapes (16 colours, bold, underline) render as inline-styled spans on the first paint after a run. The on-disk file always contains ANSI-stripped text, so reloads show plain.
  • csv — sortable HTML table; click a header to sort (numeric columns detected automatically).
  • tsv — same as CSV, tab-separated.
  • jsonl — sortable HTML table with the union of top-level keys as columns (alphabetical); nested values render as compact JSON strings.

All table renderers cap displayed rows at 1000 with a "showing 1000 of N" notice. The full data stays in the .md file.

Triggering table rendering

Add out=csv, out=tsv, or out=jsonl to the command's info string:

```sh out=csv
psql -c "select * from users" --csv
```

```sh out=tsv
awk 'BEGIN{OFS="\t"} {print $1, $3, $5}' data.txt
```

```sh out=jsonl
kubectl get pods -o json | jq -c '.items[]'
```

When you run the cell, the output block is written with the matching type= and renders as a sortable table. Without the hint, output renders as plain text (the default).

If you forget the hint and the output looks tabular, you can hand-edit type=text in the output block to type=csv / type=tsv / type=jsonl and reload.

Working in the browser

  • Run — each command cell has a Run button. Output is spliced into the .md file when it completes.
  • Run all / run ↓ — run every cell from the top, or from one cell downward. Both confirm first and stop at the first non-zero exit, since a notebook is usually a chain and continuing past a failed stage produces plausible-looking wrong results. Use cmd || true for a cell that should survive failure. Interrupt aborts the batch.
  • Edit prose — hover over a prose paragraph and click edit to swap to a textarea. Save persists immediately.
  • Edit sh cells — only when the notebook has editable: true in its YAML front matter. Each cell gets an edit button next to Run; click to swap to a textarea, type a new command, save. Without the flag, the edit button isn't shown and the endpoint returns 403 — the safe default for shared / demo notebooks.
  • Change output format — only with editable: true. A dropdown next to the edit button lets you pick text / csv / tsv / jsonl after the fact. Selecting a value rewrites BOTH the command's out= attribute AND the existing output block's type=, so the on-disk file stays internally consistent and the next run will save with the new type automatically. Useful when you ran a command, saw the output was tabular, and want to reformat without re-running.
  • + sh cell / + prose — buttons at the bottom of the notebook append a new block and open its editor immediately (empty textarea, focused). For sh cells this only works with editable: true; without the flag the new cell appears in view mode. Prose always opens in edit mode.
  • Delete (×) — every block shows a delete button. Sh cells and orphan output blocks need editable: true to delete; prose can always be deleted. A confirmation dialog (window.confirm) appears before the deletion. Deleting an sh cell also removes its paired output block.
  • Interrupt — visible top-right while a cell is running; sends SIGINT to the foreground process group.

Images

Files next to the notebook are served, so a plain markdown image link renders in the browser the same way it does on GitHub:

```sh
mytool --format svg --out chart.svg
```

![chart](chart.svg)

Only the notebook's own directory is reachable — traversal above it (including percent-encoded and via symlinks) and dotfiles are refused, and served files are sandboxed via CSP so an SVG carrying <script> stays inert.

Output: stdout vs stderr

A command's stdout and stderr are captured separately. The saved output block contains:

  • on exit 0: stdout — but if stdout is empty, stderr instead. Many commands succeed with their output on stderr (tool --version, --help, informational messages), so this avoids a blank cell. When stdout has content, stderr is still discarded, so genuine noise (progress bars, warnings) stays hidden.
  • on exit non-zero: stderr (the error message) — falling back to stdout if stderr is empty (e.g., false).

Each branch prefers the stream that normally carries the useful content and falls back to the other, so a cell never renders blank while the other stream has something to show. If you need both streams together, redirect explicitly: cmd 2>&1.

This is a deliberate departure from the v1 spec (which merged the streams). The editable: true format picker doesn't change which stream was saved — that's decided at run time by the exit code.

Limitations

  • Single user, single notebook per server process.
  • exit N inside a cell will terminate the persistent shell. Use return N (inside a function) or false / ( ... ; exit N ) if you need a non-zero status without killing the session.
  • The in-memory notebook is the source of truth during a session. External edits to the .md file while the server is running will be overwritten on next save.
  • Interactive TUI commands (vim, less, htop) will hang the cell — use the Interrupt button to recover.
  • ANSI colour is a live-render nicety; reloaded notebooks show plain text.
  • Output is capped at 1 MiB per cell. Commands that produce more keep running; the excess is dropped and truncated=true is recorded.

Building from source

go test ./...
go build ./cmd/clinote
./clinote path/to/notebook.md

Requires Go 1.25+. The dependencies are minimal: Echo (HTTP), goldmark (prose rendering), yaml.v3, creack/pty, x/sys.

Directories

Path Synopsis
cmd
clinote command
internal
render
Package render converts output-block bodies into safe HTML for the browser.
Package render converts output-block bodies into safe HTML for the browser.
runner
Package runner spawns a persistent interactive shell under a pty and runs commands inside it.
Package runner spawns a persistent interactive shell under a pty and runs commands inside it.
server
Package server hosts the HTMX-driven UI: it renders the notebook, accepts run/edit requests, and saves the notebook back to disk after each mutation.
Package server hosts the HTMX-driven UI: it renders the notebook, accepts run/edit requests, and saves the notebook back to disk after each mutation.
version
Package version reports which build of clinote is running.
Package version reports which build of clinote is running.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL