Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `pena` CLI (`apps/cli`): `server start|stop|status`, `asset upload`, `collection list|create|rename|delete`, `doc list|show|publish|rename|move|archive|unarchive|versions|restore`, `feedback show|wait|watch`, and `skill install`. `doc publish` uploads referenced local images and resolves ETag preconditions; exit codes distinguish usage errors, precondition failures, and `feedback wait` timeouts
- The server serves the built web app, so one process on port 8788 handles both the review UI and the API (`PENA_WEB_DIR` overrides the directory; a missing build runs API-only)
- Root scripts `pnpm start` (built server in the foreground) and `pnpm pena` (the CLI without linking)

### Changed

- **Breaking:** workspaces are replaced by collections, optional folders that nest. A document lives at the root or in one collection, and its slug is global
- **Breaking:** document URLs move to `/docs/<slug>` in the browser and `/api/docs/<slug>` in the API; collections live at `/collections` and `/api/collections`
- **Breaking:** the skill scripts drop `--workspace`; `publish-document.mjs` gains `--collection <slug>` and `--root`, and the publish body accepts an optional `collectionSlug`
- **Breaking:** document URLs move to `/docs/<slug>` in the browser and `/api/docs/<slug>` in the API; collections live at `/collections` and `/api/collections`; the publish body accepts an optional `collectionSlug`
- **Breaking:** the Claude Code skill is rewritten on top of the `pena` CLI. The curl instructions and the `publish-document.mjs` / `watch-feedback.mjs` scripts are gone; install it with `pena skill install`
- **Breaking:** review URLs move from the Vite dev server to the built app on port 8788 (`http://127.0.0.1:8788/docs/<slug>`); `pnpm dev` remains the two-process mode for working on Pena itself
- The database migrates to schema 10: documents from the `default` workspace move to the root, every other workspace becomes a root collection, and the migration refuses to run if a document slug exists in more than one workspace

## [0.0.2] - 2026-08-02
Expand Down
9 changes: 6 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ Contributions are welcome — bug reports, feature requests, and pull requests.

# SetUp

Follow the SetUp section in [README.md](README.md). In short: Node >= 24, `pnpm install`, `pnpm dev`.
Follow the SetUp section in [README.md](README.md). In short: Node >= 24, `pnpm install`, `pnpm build`, then `pnpm dev` while working on the server or web app.

# The Workspace Layout

| Package | What it is |
|---|---|
| `apps/server` | Fastify API with SQLite persistence and filesystem image assets |
| `apps/server` | Fastify API with SQLite persistence and filesystem image assets; serves the built web app |
| `apps/web` | React + Vite review interface |
| `packages/contracts` | Shared Zod schemas between server and web |
| `apps/cli` | The `pena` command — a client over the API, built on `node:util` `parseArgs` and global `fetch` with no third-party runtime dependencies |
| `packages/contracts` | Shared Zod schemas between server, web, and CLI |

`@pena/contracts` must be built before the other packages run — `pnpm dev`, `pnpm test`, and `pnpm typecheck` at the root already handle this.

Expand All @@ -26,6 +27,8 @@ pnpm test

Run a single package with `pnpm --filter @pena/web test`, and typecheck with `pnpm typecheck`.

The CLI suite boots the server in-process from `apps/server/dist` and spawns the built binary for `server start` and `feedback watch`, so `pnpm --filter @pena/cli test` rebuilds contracts, server, web, and CLI first; it works on a fresh clone.

# Pull Requests

- Keep a PR to one concern.
Expand Down
2 changes: 1 addition & 1 deletion OPENSOURCE_TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Written at the root, covering:
- [x] What Pena is, in two sentences.
- [ ] A screenshot or short demo GIF of the review flow — pending item 3 (the asset work in progress); a `<!-- TODO -->` marker sits where it goes.
- [x] Requirements: Node >= 24 (`.nvmrc`), pnpm.
- [x] Quick start: `pnpm install` then `pnpm dev`, and the URLs (web at `127.0.0.1:5173`, API at `127.0.0.1:8788`).
- [x] Quick start: `pnpm install`, `pnpm build`, `pnpm link --global`, then `pena server start` (web app and API at `127.0.0.1:8788`).
- [x] How to install the Claude Code skill from `resources/skills/pena/`.
- [x] Configuration: `PORT`, `PENA_DB_PATH`, and `PENA_ASSETS_DIR` env vars, with default storage in `.db/pena.sqlite` and `.assets`.
- [x] Security note: the server binds to `127.0.0.1` only and has no auth — it is a local tool, do not expose it to a network.
Expand Down
100 changes: 81 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,43 @@ cd pena
pnpm install
```

## 3. Run it
## 3. Build

```bash
pnpm dev
pnpm build
```

verify: `apps/web/dist/index.html` and `apps/cli/dist/index.js` exist.

## 4. Put `pena` on your PATH

```bash
pnpm link --global
```

verify: `pena --help` prints the command list.

If pnpm complains that its global bin directory is not in `PATH`, run `pnpm setup`, open a new shell, and link again. Skipping the link works too — run every command below as `pnpm --silent pena ...` from the repo root instead (`--silent` keeps pnpm's banner out of `--json` output).

## 5. Start the server

```bash
pena server start
```

verify: the web app is at `http://127.0.0.1:5173` and the API server prints `Pena SERVER is running at http://127.0.0.1:8788`.
verify: it prints `Pena is running at http://127.0.0.1:8788`, and that URL opens the web app. `pena server status` and `pena server stop` manage it afterward.

## 4. Install the Claude Code skill
## 6. Install the Claude Code skill

The skill is how Claude Code talks to Pena — it teaches the agent to publish documents, read feedback, and browse the archive.

```bash
mkdir -p ~/.claude/skills/pena
cp -R resources/skills/pena/. ~/.claude/skills/pena/
pena skill install
```

verify: in a new Claude Code session, ask it to *"publish this plan to Pena"* — it should respond with a `http://127.0.0.1:5173/docs/...` URL.
verify: in a new Claude Code session, ask it to *"publish this plan to Pena"* — it should respond with a `http://127.0.0.1:8788/docs/...` URL.

If you upgraded Pena from a version that had workspaces, reinstall the
skill with the same commands: its script flags and URLs changed.
If you upgraded from a version whose skill used curl and node scripts, run `pena skill install` again: the skill now drives the `pena` CLI and the review URLs moved to port 8788.

# How To Use

Expand All @@ -66,10 +82,10 @@ skill with the same commands: its script flags and URLs changed.
3. Submit the feedback. The active Claude Code session picks it up
automatically, applies the comments, and republishes to the same slug.

Claude starts one background feedback monitor after it publishes the document.
The monitor stops when that Claude Code session ends. When the Monitor tool is
not available, Pena keeps the feedback and you can still ask Claude to fetch it
manually.
Claude starts one background feedback monitor (`pena feedback watch`) after
it publishes the document. The monitor stops when that Claude Code session
ends. When the Monitor tool is not available, Pena keeps the feedback and you
can still ask Claude to fetch it manually.

Documents live at the root or inside collections, which nest like folders.
Each immutable version contains its explicit
Expand All @@ -79,16 +95,52 @@ operational metadata from the reviewed body and renders the explicit title once
inside the document surface. Earlier versions can be compared or restored. The
current Markdown can also be downloaded as a `.md` file.
Finished documents move to a browsable archive at
`http://127.0.0.1:5173/archive`; archiving pauses publishing without removing
`http://127.0.0.1:8788/archive`; archiving pauses publishing without removing
history or the download action.

# The CLI

Everything the skill does is a `pena` command, so you can do it by hand too. `pena --help` prints the full usage; `--json` on any command prints the raw result.

| Command | What it does |
|---|---|
| `pena server start [--port <n>] [--foreground]` | Start the server in the background (or attached with `--foreground`) |
| `pena server stop` | Stop a server started by the CLI |
| `pena server status` | Report whether Pena answers at the base URL |
| `pena asset upload <file>` | Upload one image and print its `/api/assets/...` URL |
| `pena collection list` | List collections with their parent and counts |
| `pena collection create <name> [--parent <slug>]` | Create a collection |
| `pena collection rename <slug> <name>` | Rename a collection |
| `pena collection delete <slug>` | Delete an empty collection |
| `pena doc list [--collection <slug\|root>] [--archived]` | List active or archived documents |
| `pena doc show <slug> [--version <n>]` | Print a document, or one historical version |
| `pena doc publish <file> --slug <slug> --title <title> [--collection <slug\|root> \| --root] [--etag <etag>] [--create] [--feedback-match <batch-id>] [--no-images]` | Upload referenced local images and publish the next version |
| `pena doc rename <slug> <title>` | Change the title (creates a version) |
| `pena doc move <slug> --to <collection-slug\|root>` | Move a document between collections |
| `pena doc archive <slug>` / `pena doc unarchive <slug>` | Archive or reactivate a document |
| `pena doc versions <slug>` | List a document's versions |
| `pena doc restore <slug> <version>` | Restore a historical version |
| `pena feedback show <slug> [--etag <etag>]` | Print every feedback batch for the current version |
| `pena feedback wait <slug> [--after <batch-id>] [--timeout <ms>]` | Block once for the next feedback submission |
| `pena feedback watch <slug> [--after <batch-id>]` | Long-poll forever, printing one JSON line per submission |
| `pena skill install [--dir <skills-dir>]` | Copy the skill into `~/.claude/skills/pena` |

Without `--create` or `--etag`, `doc publish` reads the current document first and creates it when absent or updates it against its current ETag; `--feedback-match <latestBatchId>` additionally fails with exit 3 when feedback arrived after you read it. An ETag includes its surrounding double quotes; `--etag` accepts it with or without them.

Global flags: `--url <base>` picks the server (default `PENA_URL`, then `http://127.0.0.1:8788`) and `--json` switches the output to JSON.

Exit codes: `0` success, `1` server or network error, `2` usage error (bad flag, unreadable file, invalid slug or title), `3` precondition failed (the document or its feedback changed), `4` `feedback wait` timed out.

# Configuration

| Env var | Default | Purpose |
|---|---|---|
| `PORT` | `8788` | API server port |
| `PORT` | `8788` | Server port (`pena server start --port` sets it for you) |
| `PENA_DB_PATH` | `.db/pena.sqlite` | SQLite database location |
| `PENA_ASSETS_DIR` | `.assets` | Uploaded image directory |
| `PENA_WEB_DIR` | `apps/web/dist` | Built web app the server serves; when missing, the server runs API-only |
| `PENA_URL` | `http://127.0.0.1:8788` | Base URL the CLI talks to (`--url` overrides it) |
| `PENA_STATE_DIR` | `~/.pena` | Where the CLI keeps `server.json` (the pid and URL of the server it started) and `server.log` |

Pena stores uploaded images by their content hash and does not delete them
automatically. Back up both `PENA_DB_PATH` and `PENA_ASSETS_DIR` to preserve
Expand All @@ -97,13 +149,24 @@ documents and their images.
> [!IMPORTANT]
> The server binds to `127.0.0.1` only and has no authentication. Pena is a local tool for your own machine — do not expose it to a network.

# Developing Pena

To work on Pena itself, run the two-process dev mode instead of the built server:

```bash
pnpm dev
```

It starts the API with file watching at `http://127.0.0.1:8788` and the Vite dev server at `http://127.0.0.1:5173`, which proxies `/api` to the API. Point the CLI at either one with `--url`. `pnpm start` runs the built server in the foreground after `pnpm build`.

# Architecture

A pnpm monorepo with three packages:
A pnpm monorepo with four packages:

- `apps/server` — Fastify API with SQLite persistence and filesystem image assets
- `apps/server` — Fastify API with SQLite persistence and filesystem image assets; serves the built web app
- `apps/web` — React + Vite review interface
- `packages/contracts` — shared Zod schemas between the two
- `apps/cli` — the `pena` command, a thin client over the API with no third-party runtime dependencies
- `packages/contracts` — shared Zod schemas between the three

The design documents in `docs/` cover the initial spec, storage architecture, and the feedback model — they are historical snapshots; the implementation wins where they disagree.

Expand All @@ -114,7 +177,6 @@ Rough order, subject to change:
- Keep submitted comments visible when reopening a document
- Sidebar navigation pointing to document sections
- Accept/reject flow for individual feedback items
- Separate commands for client and server so they can be deployed independently

# Contributing

Expand Down
24 changes: 24 additions & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@pena/cli",
"version": "0.0.2",
"license": "MIT",
"private": true,
"type": "module",
"bin": {
"pena": "./dist/index.js"
},
"scripts": {
"build": "tsc --build",
"pretest": "pnpm --filter @pena/contracts build && pnpm --filter @pena/server build && pnpm --filter @pena/web build && tsc --build",
"test": "vitest run src",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@pena/contracts": "workspace:*"
},
"devDependencies": {
"@pena/server": "workspace:*",
"@types/node": "^24.10.0",
"vitest": "^4.1.10"
}
}
Loading
Loading