Files

1 line
29 KiB
JSON

{"repo": "christianhelle/puny", "files": {"README.md": {"a44733e0d9e6304a45876c505c504b8157742872": {"t": 1785596380.026855, "n": 28538, "h": "2213c156cd016bd0", "c": "# Puny\n\nPuny is a minimal natively compiled single-binary coding agent with a ~1 MB footprint.\n\nIt's designed for people who want a fast, lightweight coding agent that runs smoothly \non limited hardware or remote machines. This is a coding agent that starts in under 1\nmillisecond, uses around 1 MB of disk space, uses minimal memory, uses <1% CPU,\nand stays out of your way.\n\n## Features\n\n- **Multiple providers**: local-first LM Studio, or hosted models via OpenCode Zen, OpenCode Go, or your GitHub Copilot subscription.\n- **Interactive model picker**: choose the model to load when Puny starts.\n- **Multi-turn chat**: keeps the conversation history across messages.\n- **Session management**: each run and `/reset` creates a new UUID-identified session, with the conversation automatically saved after every turn and PRDs saved to the session folder. Sessions can be resumed with `/resume` or `--session`.\n- **Tool calling**: the LLM can use built-in tools to work with your project.\n- **Skills system**: extend Puny with reusable prompt-engineering skills stored in markdown files.\n- **Built-in tools**:\n - Read, write, and list files in your project\n - Run shell commands\n - Search your codebase\n - Load skills\n - Fetch web pages\n\n## Why fast and small matters\n\nStartup time ~1ms means Puny is always ready when you are — no spinner, no animations, no\nwaiting. The ~1 MB binary and minimal memory footprint mean it runs comfortably on\na Raspberry Pi, a slow remote server over SSH, or a cheap decade-old laptop. \nEvery millisecond and megabyte is deliberate: there is no hidden runtime, no garbage collector, \nno Node.js dependencies, no JavaScript.\n\n## Why the feature set is intentionally limited\n\nPuny is designed around the way I work. It is highly opinionated. That means it will\nnever try to be everything to everyone. It's a tool and will take credit itself for your work\nby adding itself as a co-author to your commits. It will never try to be a platform, a framework, \nor a plugin ecosystem. \n\nOther coding agents include MCP, subagents, plugins, extensions, animations, and dozens\nof other features I never asked for. Puny does not. The feature set is limited to what\nI need to get results: read files, write files, run commands, search code, load skills,\nfetch web pages. If you want parallel work, run another instance.\n\nEach feature earns its place. Nothing ships because it looks good on a comparison table.\nThis is not a platform — it is a tool.\n\nPuny does not show the model's reasoning or thinking output by default. I do not care\nabout the model's internal monologue. I see it as a wall of text, a huge TL;DR you have\nto scroll past to get to the actual answer. If you want it, pass `--show-thinking`.\nIf you think you want it later, pass `--chat-log` which saves the entire conversation,\nnoise included, to puny_chat.log\n\nThe four supported providers are the ones I use personally:\n\n- [LM Studio](https://lmstudio.ai/) — local inference on my own hardware\n- [OpenCode Zen](https://opencode.ai/zen) — wide selection of reliable optimized models\n- [OpenCode Go](https://opencode.ai/go) — cheaper hosted models when I need them\n- [GitHub Copilot](https://github.com/features/copilot) — comes with my GitHub subscription\n\nPuny lets you chat with an LLM and gives it a curated set of coding tools so it can\nread, edit, search, and inspect your codebase.\n\n## Screenshots\n\n| Welcome screen | Provider picker | Model picker |\n| ---------------------------- | ------------------------------------ | --------------------------------- |\n| ![](docs/images/welcome.png) | ![](docs/images/change-provider.png) | ![](docs/images/change-model.png) |\n\n## Installation\n\n### Quick Install (Recommended)\n\n**Linux/macOS:**\n\n```bash\ncurl -fsSL https://christianhelle.com/puny/install | bash\n```\n\n**Windows (PowerShell):**\n\n```powershell\nirm https://christianhelle.com/puny/install.ps1 | iex\n```\n\nThe install scripts download the latest release from GitHub and install the binary (Linux/macOS detects x86_64 and aarch64; Windows currently installs the x86_64 build).\n\n**Custom installation directory:**\n\n```bash\n# Linux/macOS\ncurl -fsSL https://christianhelle.com/puny/install | bash -s -- --dir \"$HOME/.local/bin\"\n# Windows\n$install = irm https://christianhelle.com/puny/install.ps1\n& ([scriptblock]::Create($install)) -InstallDir \"C:\\Tools\"\n```\n\n**Pin a specific version:**\n\n```bash\n# Linux/macOS\nVERSION=v0.1.0 curl -fsSL https://christianhelle.com/puny/install | bash\n\n# Windows\n$install = irm https://christianhelle.com/puny/install.ps1\n& ([scriptblock]::Create($install)) -Version \"v0.1.0\"\n```\n\n### Upgrade\n\nIf Puny is already installed, upgrade to the latest version:\n\n```bash\npuny --upgrade\n```\n\nThis re-runs the install script for your platform.\n\n### Build from source\n\nRequires [Zig](https://ziglang.org/) 0.16.0 or later.\n\n```bash\ngit clone https://github.com/christianhelle/puny.git\ncd puny\nzig build\n```\n\nThe compiled binary is written to `zig-out/bin/puny`.\n\n### Docker\n\nPuny is published as a container image to both Docker Hub and GitHub Container Registry.\n\n```bash\ndocker pull christianhelle/puny:latest\ndocker run -it --mount \"type=bind,source=${PWD},target=/app\" christianhelle/puny\n```\n\nSee the [Docker section](#docker-1) below for detailed usage.\n\n## Quick start\n\n### LM Studio\n\nStart LM Studio and load a model with tool-calling support, then:\n\n```bash\npuny\n```\n\nOr, if you are running from the source tree:\n\n```bash\nzig build run\n```\n\n### OpenCode Zen\n\nSign in to [OpenCode Zen](https://opencode.ai/zen), copy your API key, then:\n\n```bash\npuny --provider opencode --api-key YOUR_API_KEY\n```\n\nPuny connects to `https://opencode.ai/zen` and shows the model picker.\nModels served over OpenCode Zen's OpenAI-compatible `/v1/chat/completions`\ntransport are listed (DeepSeek, GPT, GLM, Kimi, MiniMax, Grok, Big Pickle, and the free models),\nplus Qwen and Claude models served over Anthropic's `/v1/messages` transport,\nand Gemini models served over Google's `/v1/models/<model>:streamGenerateContent` transport.\n\n### OpenCode Go\n\nSign in to [OpenCode Zen](https://opencode.ai/zen), subscribe to Go, copy your API key (same key for Zen and Go), then:\n\n```bash\npuny --provider opencode-go --api-key YOUR_API_KEY\n```\n\nPuny connects to `https://opencode.ai/zen/go` and shows the model picker.\nGo models are served over OpenAI-compatible `/v1/chat/completions` (DeepSeek, Grok, GLM, Kimi, MiMo)\nand Anthropic `/v1/messages` (MiniMax, Qwen) transports.\n\n### GitHub Copilot\n\nUse Puny with your existing [GitHub Copilot](https://github.com/features/copilot) subscription:\n\n```bash\npuny --provider copilot\n```\n\nPuny resolves a GitHub OAuth token in this order:\n\n1. A token you supply manually via `--api-key`, `--api-key-file`, `PUNY_API_KEY`,\n `config.json`, or the `GITHUB_COPILOT_OAUTH_TOKEN` environment variable.\n2. Auto-discovery of an existing token from the GitHub Copilot editor plugin\n (`apps.json`/`hosts.json`) or from OpenCode's `auth.json`.\n3. An interactive device-flow login: Puny prints a code and a URL to open in your\n browser, then persists the acquired token to `config.json` for future runs.\n\nIt then exchanges that OAuth token for a short-lived Copilot token and shows the model\npicker. The picker lists the same curated models the GitHub Copilot CLI offers — the\nmodels your subscription marks as picker-enabled that are served over the OpenAI-compatible\n`/chat/completions` endpoint. Legacy models (e.g. GPT-3.5, GPT-4o), internal agent models,\nand `/responses`-only models (e.g. GPT-5.5, GPT-5 Codex) are filtered out because Puny\ncan't drive them. The general-purpose `GH_TOKEN`/`GITHUB_TOKEN` environment variables are\nintentionally **not** used, so an unrelated GitHub token can't break the exchange.\n\nSupport for GitHub Copilot is experimental, but both chat and tool calling work across the\nlisted models (Claude, GPT-5 mini, Gemini, Kimi, and more).\n\n## Docker\n\nPuny is published as a container image to both Docker Hub and GitHub Container Registry.\n\n### Pull the image\n\nFrom Docker Hub:\n\n```bash\ndocker pull christianhelle/puny:latest\n```\n\nFrom GitHub Container Registry:\n\n```bash\ndocker pull ghcr.io/christianhelle/puny:latest\n```\n\n### Run interactively\n\nMount your project directory into `/app` and allocate a TTY so Puny can read and edit files:\n\n```bash\ndocker run -it --mount \"type=bind,source=${PWD},target=/app\" christianhelle/puny\n```\n\nPuny starts in the current directory and shows the model picker.\n\n### One-shot prompt\n\nRun a single prompt and exit:\n\n```bash\ndocker run --mount \"type=bind,source=${PWD},target=/app\" christianhelle/puny --prompt \"List all source files\" --oneshot\n```\n\n### LM Studio\n\nLM Studio must be reachable from inside the container. If it is running on the Docker host, use the host's address or `host.docker.internal` on Docker Desktop:\n\n```bash\ndocker run -it --mount \"type=bind,source=${PWD},target=/app\" christianhelle/puny --url http://host.docker.internal:1234\n```\n\n### OpenCode Zen\n\n```bash\ndocker run -it --mount \"type=bind,source=${PWD},target=/app\" christianhelle/puny --provider opencode --api-key YOUR_API_KEY\n```\n\n### OpenCode Go\n\n```bash\ndocker run -it --mount \"type=bind,source=${PWD},target=/app\" christianhelle/puny --provider opencode-go --api-key YOUR_API_KEY\n```\n\n### GitHub Copilot\n\nPass a discovered or manually issued GitHub OAuth token via `PUNY_API_KEY` (device-flow\nlogin needs an interactive terminal). Replace `gho_...` below with your actual token:\n\n```bash\n# Replace gho_... with your GitHub OAuth token\ndocker run -it --mount \"type=bind,source=${PWD},target=/app\" -e PUNY_API_KEY=gho_... christianhelle/puny --provider copilot\n```\n\n### Available tags\n\n- `latest`\n- Semantic versions: `v1.2.3`, `1.2`, `1`\n- Branch refs\n\n### Build the image locally\n\nThe Dockerfile expects a Linux binary at `artifacts/puny`. Build it with the Docker step:\n\n```bash\nzig build docker\nmkdir -p artifacts\ncp zig-out/bin/puny artifacts/puny\ndocker build -t puny:local .\n```\n\n`zig build docker` is equivalent to `zig build -Doptimize=ReleaseSmall -Dtarget=x86_64-linux -Ddocker` and sets the default LM Studio URL to `http://host.docker.internal:1234`.\n\nRun the locally built image the same way as the published one:\n\n```bash\ndocker run -it --mount \"type=bind,source=${PWD},target=/app\" puny:local\n```\n\n### API key security\n\nThe examples above pass `--api-key` inline for simplicity. For shared or production environments, prefer mounting a key file with `--api-key-file` or a `config.json` instead.\n\n## Usage\n\nMake sure LM Studio is running and a tool-capable model is loaded, then start Puny:\n\n```bash\npuny\n```\n\nPuny shows the model picker, connects to LM Studio, and drops you into a chat prompt.\n\n### Interactive chat\n\nType your request and press Enter:\n\n```text\nPrompt: Explain what this project does\n```\n\nThe model replies in the terminal. You can keep sending follow-up messages; Puny remembers the conversation.\n\n```text\nPrompt: Now list the source files\n\n🔧 Listing directory \"src\"\n\nThe project has source files under src/, including main.c, utils.h, and a tests/ folder.\n```\n\n### One-shot prompt\n\nRun a single prompt and exit. Useful for scripts or quick tasks:\n\n```bash\npuny --prompt \"List all source files\" --oneshot\n```\n\n### Select a provider\n\nUse `--provider` to switch between LM Studio (`lmstudio`, the default), OpenCode Zen (`opencode`), OpenCode Go (`opencode-go`), and GitHub Copilot (`copilot`). You can also set `PUNY_PROVIDER` or the `provider` field in `config.json`.\n\n```bash\npuny --provider opencode --api-key YOUR_API_KEY\n```\n\nPrecedence is: `--provider` > `PUNY_PROVIDER` > `config.json` > `lmstudio`.\n\n### Connect to a remote LM Studio instance\n\nIf LM Studio is running on another machine, point Puny at it:\n\n```bash\npuny --url http://192.168.1.42:1234\n```\n\n### Authenticate\n\nIf your provider requires an API token, provide it via CLI, environment variable, config file, or `--reconfigure`:\n\n```bash\n# CLI flag (session only)\npuny --api-key lmstudio-token-123\n\n# Environment variable (session only)\nexport PUNY_API_KEY=lmstudio-token-123\npuny\n\n# Read from a file (session only)\npuny --api-key-file /run/secrets/lmstudio-key\n\n# Save to config interactively\npuny --reconfigure\n```\n\nPrecedence is: `--api-key` > `--api-key-file` > `PUNY_API_KEY` > `config.json`.\n\nOpenCode Zen requires an API key. Puny exits early with a hint if the key is missing.\n\nGitHub Copilot does not need an API key up front — Puny discovers an existing GitHub\nOAuth token or runs a device-flow login on first use, then persists it. You can still\nsupply a token manually (via `--api-key`, `PUNY_API_KEY`, or `GITHUB_COPILOT_OAUTH_TOKEN`)\nto skip discovery and login.\n\n### Save provider and API key to config\n\nRun `--reconfigure` to choose a provider and save its URL and API key to `config.json`:\n\n```bash\npuny --reconfigure\n```\n\nYou will be prompted for:\n\n1. **Provider** — `lmstudio`, `opencode`, `opencode-go`, or `copilot`.\n2. **Provider URL** — press Enter to use the provider's default. (OpenCode Zen's URL is fixed at `https://opencode.ai/zen`; GitHub Copilot's is fixed at `https://api.githubcopilot.com`.)\n3. **API key** — press Enter to keep the existing key, or `-` to clear it.\n\nOnce saved, Puny uses the stored provider and key on subsequent runs, so you only need to pass `--provider` or `--api-key` again if you want to override them for a single session. If an OpenCode Zen request fails with an authentication error, Puny prints an auth hint; use `--reconfigure` to update the key.\n\n`--url` and `PUNY_PROVIDER_URL` only affect LM Studio; OpenCode Zen always uses `https://opencode.ai/zen` and GitHub Copilot always uses `https://api.githubcopilot.com`.\n\nThe config file stores per-provider settings (URL, API key, and last-selected model) so you can switch between providers without re-entering credentials.\n\n## Skills\n\nPuny can load reusable prompt-engineering skills from markdown files. Skills are\ninstructions that get injected into the system prompt so the model knows how to\nbehave for a specific task. They can be loaded by slash command, by mentioning\na trigger phrase in your message, or by the model itself via tool call.\n\n### Skill locations\n\nPuny scans two directories for skills:\n\n- **Global**: `~/.agents/skills/` — skills available across all repositories\n- **Repository**: `<repo-root>/.agents/skills/` — project-specific skills (auto-detected via `git rev-parse --show-toplevel`)\n\nEach subdirectory inside these paths is treated as a skill. The skill's name is the\ndirectory name.\n\n### Skill structure\n\nA skill directory must contain a `SKILL.md` file. The file starts with YAML frontmatter\nthat can include the following fields:\n\n```markdown\n---\nname: my-skill\ndescription: >\n Expert knowledge of MyTool for integration testing.\n Covers setup, configuration, and common patterns.\ntriggers: mytool, integration test, configure\ndisable-model-invocation: false\n---\n\n# MyTool Instructions\n\nWhen asked about MyTool, follow these guidelines...\n```\n\n| Field | Required | Description |\n| -------------------------- | -------- | ------------------------------------------------------------------------------------------ |\n| `name` | Yes | Canonical identifier (must match the directory name) |\n| `description` | Yes | Short summary shown in `/skills` output and the `<available_skills>` block |\n| `triggers` | No | Comma-separated phrases that auto-load the skill when mentioned in a user message |\n| `disable-model-invocation` | No | Set to `true` to prevent the model from loading this skill via tool call (default `false`) |\n\nThe body after the frontmatter is the skill content that gets injected into the\nconversation when the skill is loaded.\n\n### Loading a skill\n\nSkills can be loaded in three ways:\n\n1. **Slash command** — type `/<skill-name>` in the prompt:\n\n ```text\n Prompt: /nano-commits\n ```\n\n2. **Keyword trigger** — mention a trigger phrase from the skill's `triggers` field in your\n message. For example, a `caveman` skill with `triggers: talk like caveman, be brief`\n loads automatically when you say:\n\n ```text\n Prompt: talk like caveman from now on\n ```\n\n Skills are also triggered by their directory name as a whole word — saying\n `grill me on this design` loads the `grill-me` skill.\n\n3. **Model invocation** — the model can load a skill it considers relevant by calling the\n `load_skill` tool. This happens automatically when the model sees a matching skill in\n the `<available_skills>` block. Skills with `disable-model-invocation: true` won't\n appear for model invocation and must be loaded by slash command only.\n\n### Listing available skills\n\nUse `/skills` to list all discovered skills from both global and repository locations:\n\n```text\nPrompt: /skills\n\nAvailable skills:\n\n nano-commits\n grill-me\n```\n\nSkills with a `description` field in their frontmatter show a description next to\ntheir name.\n\n### How skills work\n\nWhen Puny starts, it scans both skill directories and builds a registry. Available\nskills and their descriptions (if scanned) are injected into the system prompt as an\n`<available_skills>` block:\n\n```xml\n<available_skills>\n <skill>\n <name>nano-commits</name>\n <description>Commit often. One logical change per commit.</description>\n </skill>\n</available_skills>\n```\n\nWhen a skill is loaded (via slash command, keyword trigger, or model invocation),\nPuny reads the `SKILL.md` body, strips the frontmatter, and adds the content as a\nsystem message. The model can then follow the skill's instructions for the remainder\nof the conversation.\n\nSkills stay loaded for the session. To clear all loaded skills, use `/reset`.\n\n## Tool calling\n\nPuny sends a list of available tools to the model on every request. When the model decides to call a tool, Puny executes it automatically and feeds the result back into the conversation.\n\nTool-call status lines use concise action-oriented summaries instead of raw JSON:\n\n```text\n🔧 Reading \"src/main.zig\"\n🔧 Running \"zig build test\"\n🔧 Writing 12 lines (384 bytes) to \"README.md\"\n```\n\nLarge payloads, such as file writes, are summarized rather than printed in full.\n\n### ⚠️ Safety warning - YOLO mode by default\n\nTools execute **automatically without confirmation**. This includes file writes (which overwrite files) and shell commands (which run arbitrary commands). Only run Puny in directories where you are comfortable with the model making changes.\n\n## Reference\n\n### CLI options\n\n| Flag | Description |\n| ----------------------- | ----------------------------------------------------------------------------------------- |\n| `--provider <name>` | Provider: `lmstudio`, `opencode`, `opencode-go`, or `copilot` (env/config/CLI precedence) |\n| `-u`, `--url <url>` | LM Studio endpoint URL (default: `http://127.0.0.1:1234`) |\n| `-k`, `--api-key <key>` | Provider API token (session only) |\n| `--api-key-file <path>` | Read provider API token from file (session only) |\n| `--chat-log` | Save full conversation (including reasoning) to `puny_chat.log` |\n| `-m`, `--model <id>` | Model identifier (skips picker if found in running models) |\n| `-p`, `--prompt <text>` | Pre-fill prompt as first user message |\n| `-1`, `--oneshot` | Exit after processing the prompt (requires `--prompt`) |\n| `-M`, `--mock` | Use mock provider (no backend required) |\n| `--reconfigure` | Re-run first-run setup and update config |\n| `--show-thinking` | Show reasoning/thinking output from the model |\n| `--session <id>` | Resume a previous session by UUID or unique prefix |\n| `--resume` | Resume the most recent session with a saved conversation |\n| `--prune` | Delete old sessions (use with `--session` to keep one) |\n| `--debug` | Log HTTP requests and responses to `puny_debug.log` |\n| `-U`, `--upgrade` | Upgrade to the latest release via install script |\n| `-h`, `--help` | Show help text |\n| `-V`, `--version` | Print version |\n\n### Environment variables\n\n| Variable | Description |\n| ---------------------------- | --------------------------------------------------- |\n| `PUNY_CHAT_LOG` | Set to `1` or `true` to save full conversation to `puny_chat.log` |\n| `PUNY_PROVIDER` | Default provider name (overrides config) |\n| `PUNY_PROVIDER_URL` | LM Studio endpoint URL (overrides config/CLI) |\n| `PUNY_API_KEY` | Provider API token (overrides config, session only) |\n| `PUNY_MODEL` | Default model identifier (overrides config) |\n| `PUNY_MOCK` | Set to `1` or `true` to enable mock provider |\n| `PUNY_SHOW_THINKING` | Set to `1` or `true` to show reasoning output |\n| `GITHUB_COPILOT_OAUTH_TOKEN` | GitHub OAuth token for Copilot provider |\n\n### Interactive commands\n\nWhile in a chat session:\n\n- `/quit` or `/exit` — exit Puny\n- `/reset` — clear the conversation, start a new session, and unload all skills\n- `/stats` — show session statistics and memory usage\n- `/config` — reconfigure provider, URL, and API key mid-session; changing the provider rebuilds the connection and re-opens the model picker\n- `/plan [task]` — enter planning mode (optionally with a task description); the resulting PRD is saved to the session folder as `plan.md`\n- `/build [task]` — switch to build mode (optionally with a task description)\n- `/model [id]` — switch to another model; shows the model picker if no ID is given\n- `/provider [name]` — switch to another provider without reconfiguring everything; shows the provider picker if no name is given, then opens the model picker for the new provider\n- `/sessions` — list all saved sessions, showing their UUID, whether they have a `plan.md` or saved conversation, and a preview of the first user message\n- `/resume [id]` — list saved sessions and pick one to restore, or restore a specific session by UUID prefix\n- `/prune` — delete all session directories except the current one\n- `/skills` — list all available global and repository skills\n\nAny unrecognized slash command (e.g. `/nano-commits`, `/grill-me`) is treated as a\nskill name and loads the matching skill if found.\n\nSkills also load automatically when your message contains the skill's directory name\nor a trigger phrase listed in its frontmatter `triggers` field. The model can also\nload skills on its own via the `load_skill` tool.\n\n### Sessions\n\nEach Puny session is identified by a UUID. A new session is created every time you\nstart Puny or run `/reset`. The session ID is shown in the welcome screen and in\nthe `/stats` header.\n\n#### Storage\n\nSessions are stored at `~/.config/puny/sessions/<uuid>/` (Linux/macOS) or\n`%APPDATA%\\puny\\sessions\\<uuid>\\` (Windows). The base directory is configurable:\nuse `$XDG_CONFIG_HOME/puny` when set on Linux/macOS, and on Windows fall back to\n`%USERPROFILE%\\puny` when `%APPDATA%` is unavailable. Each session folder can contain:\n\n| File | Description |\n| --------------- | --------------------------------------------------------------- |\n| `plan.md` | PRD markdown produced by the model during `/plan` mode |\n| `plan.html` | HTML version of the PRD |\n| `messages.json` | Full conversation history, saved automatically after every turn |\n| `session.json` | Session metadata (planning mode, first user prompt) |\n\n#### Conversation persistence\n\nThe entire conversation is saved to `messages.json` in the session folder after\nevery completed chat turn. It is also saved automatically when you exit the\nsession (`/quit`, `Ctrl+C`) or run `/reset`. Tool call results are included in\nthe save so the restored session has the full context.\n\n#### Restoring a session\n\nYou can resume a previous session in several ways:\n\n- **CLI flag**: `puny --session <uuid-or-prefix>` loads a specific session by\n UUID or unique prefix. Use `--session` with a partial UUID for prefix matching\n (e.g. `puny --session abc-12` matches `abc-1234-...`).\n- **CLI flag**: `puny --resume` resumes the most recent session with a saved\n conversation. If multiple sessions have saved conversations, a hint is printed.\n- **CLI flag**: `puny --prune` deletes all session directories. Use\n `puny --prune --session <uuid>` to delete all sessions except the one to keep.\n- **Interactive**: `/resume` inside a chat session lists all saved sessions\n that have conversations and lets you pick one to restore. You can also pass\n a prefix: `/resume abc-12`.\n- **In-session restore**: Calling `/resume` replaces the current conversation\n with the saved one, keeping the same provider and model configuration.\n\nWhen a session is restored, the system prompt, skills blocks, and planning mode\nare restored exactly as they were saved — the conversation picks up where it\nleft off.\n\n#### Planning mode\n\nIn planning mode the model can only read files, list directories, search code,\ncheck git status/diff, and fetch web pages — plus use the `save_prd` tool to\nwrite the final PRD. When the user confirms readiness, the model calls\n`save_prd` with markdown and HTML content, which writes both `plan.md` and\n`plan.html` to the session folder. Planning mode state is persisted in\n`session.json` and restored when the session is resumed.\n\n## Build from source\n\nRequires [Zig](https://ziglang.org/) 0.16.0 or later.\n\n```bash\nzig build\n```\n\nThe compiled binary is written to `zig-out/bin/puny`. Copy it to a directory on your PATH to run it from anywhere.\n\nRun the test suite:\n\n```bash\nzig build test\n```\n\nRun the regression test suite:\n\n```bash\nzig build test-regression\n```\n\n## Development / testing\n\n### Mock mode (no LM Studio, OpenCode Zen, or Github Copilot required)\n\nStart without a running AI backend:\n\n```bash\nzig build run -- --mock\n```\n\nYou can also set the `PUNY_MOCK` environment variable:\n\n```bash\nexport PUNY_MOCK=1\nzig build run\n```\n\nThe mock provider returns canned responses and simulates tool calls based on keywords in your prompt:\n\n| Prompt contains | Mock response |\n| -------------------------- | --------------------------------------------- |\n| `read`, `file`, `code` | Calls `read_file` tool |\n| `search`, `grep`, `find` | Calls `grep_search` tool |\n| `shell`, `run`, `execute` | Calls `execute_shell` tool |\n| `error`, `timeout`, `fail` | Simulates a network error |\n| _(after a tool result)_ | Returns a completion acknowledging the result |\n| _(anything else)_ | Returns a canned text response |\n\nUse `--model` to skip the model picker in mock mode:\n\n```bash\nzig build run -- --mock --model mock-model --prompt \"search for something\" --oneshot\n```\n\n### Show thinking/reasoning output\n\nTo see the model's internal reasoning (thinking) output, use `--show-thinking`:\n\n```bash\npuny --show-thinking\n```\n\nOr set the `PUNY_SHOW_THINKING` environment variable:\n\n```bash\nexport PUNY_SHOW_THINKING=true\npuny\n```\n\n### HTTP debug logging\n\nTo log all HTTP requests and responses to `puny_debug.log`, use `--debug`:\n\n```bash\npuny --debug\n```\n\nThe debug log captures request payloads, response headers, and SSE chunks for troubleshooting.\n\n## License\n\nMIT\n"}}}}