mirror of
https://github.com/JuliusBrussee/caveman.git
synced 2026-08-11 13:21:09 +02:00
Improve installer idempotency and opencode fencing
Refactor and harden the unified Node installer and related docs. Major changes: - bin/install.js: validate --only ids, expand ~ for --config-dir, make Claude/install flows async (await hooks/init), add many CLI flags (--no-mcp-shrink, --with-hooks/--no-hooks, --skip-skills, --config-dir docs), preserve original backups once, avoid overwriting plugin/command files unless --force, and add opencode AGENTS.md fenced begin/end markers so installs/uninstalls can append/strip cleanly. Also add opencode idempotency probes for claude/gemini uninstall paths and better handling of opencode plugin payload. - bin/lib/settings.js: safer removeCavemanHooks that validates shapes before mutating hooks. - runInit/installHooks/downloadTo calls made async and awaited; runInit returns promise now. - Docs and README/INSTALL/CONTRIBUTING/CLAUDE.md/src/hooks/README.md: update user-facing text to match new flags/behaviour, standardize use of $CLAUDE_CONFIG_DIR, clarify --with-init semantics, and note opencode/openclaw handling. Change command name usages from `/caveman:compress` to `/caveman-compress`. - opencode: write fenced caveman block to AGENTS.md and handle legacy unfenced blocks; tests updated to expect fence markers. These changes improve idempotency, safer upgrades/uninstalls, clearer UX around per-repo vs per-user init, and make the opencode rule block removable without destroying user content.
This commit is contained in:
@@ -83,7 +83,7 @@ caveman/
|
||||
| File | What it controls |
|
||||
|------|-----------------|
|
||||
| `skills/caveman/SKILL.md` | Caveman behavior: intensity levels, rules, wenyan mode, auto-clarity, persistence. Only file to edit for behavior changes. |
|
||||
| `src/rules/caveman-activate.md` | Always-on auto-activation rule body. Consumed by `src/tools/caveman-init.js` when a user runs `npx caveman --only <agent>`. Edit here, not in any per-agent rule copy. |
|
||||
| `src/rules/caveman-activate.md` | Always-on auto-activation rule body. Consumed by `src/tools/caveman-init.js` when a user runs `npx caveman --with-init` (per-repo IDE rule files). Edit here, not in any per-agent rule copy. |
|
||||
| `src/rules/caveman-openclaw-bootstrap.md` | Marker-fenced bootstrap snippet appended to `~/.openclaw/workspace/SOUL.md` by `bin/lib/openclaw.js`. Drives always-on caveman through the OpenClaw gateway. Must include the SENTINEL `Respond terse like smart caveman` and stay well under OpenClaw's 12K-per-bootstrap-file cap. |
|
||||
| `bin/lib/openclaw.js` | OpenClaw install/uninstall helper. Frontmatter merge (`version`, `always: true`), SOUL.md marker append/strip, idempotent. Shared by `bin/install.js` and `src/tools/caveman-init.js`. |
|
||||
| `skills/caveman-commit/SKILL.md` | Caveman commit message behavior. Fully independent skill. |
|
||||
@@ -99,7 +99,9 @@ caveman/
|
||||
|
||||
### Auto-generated / auto-synced — do not edit directly
|
||||
|
||||
We removed the agent-specific dotdir mirrors at the repo root (`.cursor/`, `.windsurf/`, `.clinerules/`, `.github/copilot-instructions.md`, root `caveman/SKILL.md`). They were never read by the installer — only used to self-apply caveman to this repo when a maintainer opened it in Cursor/Windsurf/Cline. Devs who want caveman in their editor while editing this repo should run `npx caveman --only <agent>` once. The installer generates per-user agent rule files from `src/rules/caveman-activate.md` via `src/tools/caveman-init.js`.
|
||||
We removed the agent-specific dotdir mirrors at the repo root (`.cursor/`, `.windsurf/`, `.clinerules/`, `.github/copilot-instructions.md`, root `caveman/SKILL.md`). They were never read by the installer — only used to self-apply caveman to this repo when a maintainer opened it in Cursor/Windsurf/Cline. Devs who want caveman in their editor while editing this repo should run `npx caveman --with-init` once (writes per-repo rule files from `src/rules/caveman-activate.md` via `src/tools/caveman-init.js`). For per-user installs through the upstream skills CLI, `npx caveman --only <agent>` runs `npx skills add ... -a <profile>`.
|
||||
|
||||
A handful of dotdir leftovers (`.junie/`, `.kiro/`, `.roo/`, `.agents/`) still hold a stale `cavecrew/SKILL.md` mirror from before the cleanup. They aren't read by anything in the current install path; remove on sight, no migration needed.
|
||||
|
||||
What's left is the Claude Code plugin distribution (required by the plugin loader) and the release ZIP.
|
||||
|
||||
@@ -111,7 +113,7 @@ What's left is the Claude Code plugin distribution (required by the plugin loade
|
||||
| `plugins/caveman/agents/cavecrew-*.md` | `agents/cavecrew-*.md` |
|
||||
| `dist/caveman.skill` | ZIP of `skills/caveman/` directory (gitignored; rebuilt by CI on release) |
|
||||
|
||||
Skills not in this table (`caveman-commit`, `caveman-review`, `caveman-help`, `caveman-stats`) are not mirrored into the Claude Code plugin distribution. They reach Claude Code through the standalone hook + skill install path, and reach other agents via `npx skills add`.
|
||||
Skills not in this table (`caveman-commit`, `caveman-review`, `caveman-help`, `caveman-stats`) are not mirrored into the Claude Code plugin distribution by CI. They reach Claude Code through the standalone hook + skill install path, and reach other agents via `npx skills add`. A `plugins/caveman/skills/caveman-stats/` directory is currently checked in as a hand-committed copy; the sync workflow does not touch it, so don't rely on edits there to propagate.
|
||||
|
||||
---
|
||||
|
||||
@@ -172,7 +174,7 @@ Reads JSON from stdin. Three responsibilities:
|
||||
- `/caveman` → configured default (see `caveman-config.js`, defaults to `full`)
|
||||
- `/caveman lite` → `lite`
|
||||
- `/caveman ultra` → `ultra`
|
||||
- `/caveman wenyan` or `/caveman wenyan-full` → `wenyan`
|
||||
- `/caveman wenyan` or `/caveman wenyan-full` → `wenyan` (alias) / `wenyan-full`
|
||||
- `/caveman wenyan-lite` → `wenyan-lite`
|
||||
- `/caveman wenyan-ultra` → `wenyan-ultra`
|
||||
- `/caveman-commit` → `commit`
|
||||
@@ -242,10 +244,10 @@ How caveman reaches each agent type:
|
||||
| Gemini CLI | Extension with `GEMINI.md` context file | Yes — context file loads every session |
|
||||
| opencode | Native plugin (`src/plugins/opencode/`) copied into `~/.config/opencode/plugins/caveman/` + `AGENTS.md` ruleset + skills/agents/commands directories. Plugin uses `session.created` and `tui.prompt.append` lifecycle hooks. No statusline (opencode TUI exposes no plugin-writable badge). | Yes — `session.created` writes flag, `AGENTS.md` carries always-on ruleset |
|
||||
| OpenClaw | Workspace skill at `~/.openclaw/workspace/skills/caveman/SKILL.md` (frontmatter merged with `version` + `always: true`) plus a marker-fenced bootstrap block in `~/.openclaw/workspace/SOUL.md`. Both writes go through `bin/lib/openclaw.js`; workspace path is overridable via `OPENCLAW_WORKSPACE`. | Yes — SOUL.md is auto-injected each turn under "Project Context" (subject to OpenClaw's 12K-per-file / 60K-total bootstrap caps) |
|
||||
| Cursor | Per-user `.cursor/rules/caveman.mdc` written by `src/tools/caveman-init.js` | Yes — always-on rule (after `npx caveman --only cursor`) |
|
||||
| Windsurf | Per-user `.windsurf/rules/caveman.md` written by `src/tools/caveman-init.js` | Yes — always-on rule (after `npx caveman --only windsurf`) |
|
||||
| Cline | Per-user `.clinerules/caveman.md` written by `src/tools/caveman-init.js` | Yes — Cline auto-discovers `.clinerules/` |
|
||||
| Copilot | Per-user `.github/copilot-instructions.md` written by `src/tools/caveman-init.js` + `AGENTS.md` | Yes — repo-wide instructions |
|
||||
| Cursor | `npx skills add ... -a cursor` (default via `--only cursor`) writes the upstream skill profile; per-repo `.cursor/rules/caveman.mdc` via `--with-init` (calls `src/tools/caveman-init.js`) | Yes — always-on rule |
|
||||
| Windsurf | `npx skills add ... -a windsurf` (default via `--only windsurf`); per-repo `.windsurf/rules/caveman.md` via `--with-init` | Yes — always-on rule |
|
||||
| Cline | `npx skills add ... -a cline` (default via `--only cline`); per-repo `.clinerules/caveman.md` via `--with-init` | Yes — Cline auto-discovers `.clinerules/` |
|
||||
| Copilot | `npx skills add ... -a github-copilot` (soft probe — pass `--only copilot`); per-repo `.github/copilot-instructions.md` + `AGENTS.md` via `--with-init` | Yes — repo-wide instructions |
|
||||
| Others (Junie, Trae, Warp, Tabnine, Mistral, Qwen, Devin, Droid, ForgeCode, Bob, Crush, iFlow, OpenHands, Qoder, Rovo Dev, Replit, Antigravity, …) | `npx skills add JuliusBrussee/caveman -a <profile>` | No — user must say `/caveman` each session |
|
||||
|
||||
opencode reaches Tier 1 minus the statusline (opencode's TUI has no plugin-writable badge). Mode flag lives at `~/.config/opencode/.caveman-active` for any external tooling that wants to surface it.
|
||||
|
||||
+4
-5
@@ -58,13 +58,12 @@ on every push to `main`.
|
||||
| Path | Rebuilt from |
|
||||
|------|--------------|
|
||||
| `plugins/caveman/skills/caveman/SKILL.md` | `skills/caveman/SKILL.md` |
|
||||
| `plugins/caveman/skills/caveman-commit/SKILL.md` | `skills/caveman-commit/SKILL.md` |
|
||||
| `plugins/caveman/skills/caveman-review/SKILL.md` | `skills/caveman-review/SKILL.md` |
|
||||
| `plugins/caveman/skills/caveman-compress/SKILL.md` | `skills/caveman-compress/SKILL.md` |
|
||||
| `plugins/caveman/skills/caveman-compress/{SKILL.md, scripts/}` | `skills/caveman-compress/{SKILL.md, scripts/}` |
|
||||
| `plugins/caveman/skills/cavecrew/SKILL.md` | `skills/cavecrew/SKILL.md` |
|
||||
| `plugins/caveman/agents/cavecrew-*.md` | `agents/cavecrew-*.md` |
|
||||
| `dist/caveman.skill` | ZIP of `skills/caveman/` |
|
||||
| `.codex-plugin/plugin.json` (inside any plugin distribution) | generated at package time |
|
||||
| `dist/caveman.skill` | ZIP of `skills/caveman/` (gitignored; rebuilt by CI on each push to `main`) |
|
||||
|
||||
`caveman-commit`, `caveman-review`, `caveman-help`, and `caveman-stats` are **not** mirrored under `plugins/caveman/skills/` by CI. Claude Code reaches them through the standalone hook + skill install path and `npx skills` carries them to other agents. If you see `plugins/caveman/skills/caveman-stats/` checked in, treat it as a legacy hand-committed copy — the workflow in `.github/workflows/sync-skill.yml` does not touch it.
|
||||
|
||||
When in doubt: if the file lives under `plugins/`, `dist/`, or any agent
|
||||
dotdir mirror, it's a build artifact. Edit the top-level source instead.
|
||||
|
||||
+28
-19
@@ -41,11 +41,11 @@ If you want to install for one agent (or want to know exactly what command runs
|
||||
| **Gemini CLI** | `gemini extensions install https://github.com/JuliusBrussee/caveman` | Yes |
|
||||
| **opencode** | `node bin/install.js --only opencode` *(or `npx -y github:JuliusBrussee/caveman -- --only opencode`)* | Yes (plugin + AGENTS.md) |
|
||||
| **OpenClaw** | `npx -y github:JuliusBrussee/caveman -- --only openclaw` | Yes (workspace skill + SOUL.md) |
|
||||
| **Codex CLI** | `npx skills add JuliusBrussee/caveman -a codex` | Yes (in this repo via `.codex/`) |
|
||||
| **Cursor** | `npx skills add JuliusBrussee/caveman -a cursor` | Yes (rule file) |
|
||||
| **Windsurf** | `npx skills add JuliusBrussee/caveman -a windsurf` | Yes (rule file) |
|
||||
| **Cline** | `npx skills add JuliusBrussee/caveman -a cline` | Yes (rule file) |
|
||||
| **GitHub Copilot** | `npx skills add JuliusBrussee/caveman -a github-copilot` | Yes (repo-wide instructions) |
|
||||
| **Codex CLI** | `npx skills add JuliusBrussee/caveman -a codex` | Per-session: `/caveman` |
|
||||
| **Cursor** | `npx skills add JuliusBrussee/caveman -a cursor` | Per-session by default; `--with-init` for an always-on rule file |
|
||||
| **Windsurf** | `npx skills add JuliusBrussee/caveman -a windsurf` | Per-session by default; `--with-init` for an always-on rule file |
|
||||
| **Cline** | `npx skills add JuliusBrussee/caveman -a cline` | Per-session by default; `--with-init` for an always-on rule file |
|
||||
| **GitHub Copilot** *(soft probe)* | `npx -y github:JuliusBrussee/caveman -- --only copilot --with-init` | Repo-wide instructions via `--with-init` |
|
||||
| **Continue** | `npx skills add JuliusBrussee/caveman -a continue` | No — say `/caveman` |
|
||||
| **Kilo Code** | `npx skills add JuliusBrussee/caveman -a kilo` | No |
|
||||
| **Roo Code** | `npx skills add JuliusBrussee/caveman -a roo` | No |
|
||||
@@ -72,7 +72,7 @@ If you want to install for one agent (or want to know exactly what command runs
|
||||
| **Qoder** *(soft probe)* | `npx skills add JuliusBrussee/caveman -a qoder` | No |
|
||||
| **Google Antigravity** *(soft probe)* | `npx skills add JuliusBrussee/caveman -a antigravity` | No |
|
||||
|
||||
"Soft probe" = installer won't auto-detect these without `--only <id>` because there's no reliable always-on signal. Pass the flag when you want them.
|
||||
"Soft probe" = installer won't auto-detect these without `--only <id>` because there's no reliable always-on signal (Copilot subscription state is auth-gated; the others have no CLI / config-dir-only). Pass the flag when you want them.
|
||||
|
||||
For "auto-activates? No" agents, type `/caveman` once per session (or use natural-language triggers like "talk like caveman", "caveman mode").
|
||||
|
||||
@@ -105,8 +105,14 @@ Useful flags:
|
||||
| `--minimal` | Plugin / extension only. No hooks, no MCP shrink, no per-repo rules. |
|
||||
| `--only <id>` | One agent only. Repeatable: `--only claude --only cursor`. |
|
||||
| `--dry-run` | Print every command. Write nothing. |
|
||||
| `--with-init` | Drop always-on rule files into the current repo (`.cursor/`, `.windsurf/`, `.clinerules/`, `.github/copilot-instructions.md`, `AGENTS.md`). |
|
||||
| `--with-init` | Drop always-on rule files into the current repo (`.cursor/`, `.windsurf/`, `.clinerules/`, `.github/copilot-instructions.md`, `.opencode/AGENTS.md`, `AGENTS.md`) and, if OpenClaw is on the box, append the bootstrap block to `~/.openclaw/workspace/SOUL.md`. |
|
||||
| `--with-mcp-shrink` | Register `caveman-shrink` MCP proxy. **On by default.** |
|
||||
| `--no-mcp-shrink` | Skip MCP-shrink registration. |
|
||||
| `--with-hooks` / `--no-hooks` | Force-on or force-off the Claude Code hook installer. (Default: on.) |
|
||||
| `--skip-skills` | Don't run the npx-skills auto-detect fallback when nothing else matched. |
|
||||
| `--config-dir <path>` | Claude Code config dir for hook files + `settings.json`. **Does NOT scope** `claude plugin install`, `gemini extensions install`, opencode (`XDG_CONFIG_HOME`), or openclaw (`OPENCLAW_WORKSPACE`) — those use their own paths. Default: `$CLAUDE_CONFIG_DIR` or `~/.claude`. `~` is expanded. |
|
||||
| `--non-interactive` | Never prompt; use defaults. (Auto when stdin is not a TTY.) |
|
||||
| `--no-color` | Disable ANSI colors. |
|
||||
| `--list` | Print full agent matrix and exit. |
|
||||
| `--force` | Re-run even if already installed. |
|
||||
| `--uninstall` | Remove everything. See below. |
|
||||
@@ -124,7 +130,7 @@ curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/src/rule
|
||||
> .cursor/rules/caveman.mdc # or .windsurf/rules/caveman.md, .clinerules/caveman.md, .github/copilot-instructions.md
|
||||
```
|
||||
|
||||
`--with-init` writes the rule into every supported per-agent location it can detect (`.cursor/rules/`, `.windsurf/rules/`, `.clinerules/`, `.github/copilot-instructions.md`, `AGENTS.md`). Single source: [`src/rules/caveman-activate.md`](src/rules/caveman-activate.md).
|
||||
`--with-init` writes the rule into every supported per-agent location it can detect (`.cursor/rules/`, `.windsurf/rules/`, `.clinerules/`, `.github/copilot-instructions.md`, `.opencode/AGENTS.md`, `AGENTS.md`). It also installs the OpenClaw workspace bootstrap (skill folder + SOUL.md marker block) when `~/.openclaw/workspace/` exists. Single source: [`src/rules/caveman-activate.md`](src/rules/caveman-activate.md).
|
||||
|
||||
## Verify
|
||||
|
||||
@@ -145,7 +151,7 @@ Open Claude Code, type `/caveman`. Response should be terse fragments — "Got i
|
||||
**3. Check the flag file.**
|
||||
|
||||
```bash
|
||||
cat ~/.claude/.caveman-active
|
||||
cat "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.caveman-active"
|
||||
# expected output: full
|
||||
```
|
||||
|
||||
@@ -161,15 +167,17 @@ npx -y github:JuliusBrussee/caveman -- --uninstall
|
||||
|
||||
What it removes:
|
||||
|
||||
- Caveman hook entries from `~/.claude/settings.json` (matched by the substring `caveman`).
|
||||
- Hook files from `~/.claude/hooks/caveman-*`.
|
||||
- Caveman hook entries from `$CLAUDE_CONFIG_DIR/settings.json` (default `~/.claude/`; matched by the substring `caveman`).
|
||||
- Hook files in `$CLAUDE_CONFIG_DIR/hooks/` (`caveman-activate.js`, `caveman-mode-tracker.js`, `caveman-stats.js`, `caveman-config.js`, `caveman-statusline.{sh,ps1}`, plus the dir's `package.json` marker).
|
||||
- The Claude Code plugin and the Gemini CLI extension (if installed).
|
||||
- The opencode native plugin (`~/.config/opencode/plugins/caveman/`, the `plugin` and `mcp.caveman-shrink` entries from `opencode.json`, our skill/agent/command files, the caveman block from `AGENTS.md`, and the opencode flag file).
|
||||
- The OpenClaw workspace skill folder and the marker-fenced block from `~/.openclaw/workspace/SOUL.md` (when present).
|
||||
- The `.caveman-active` flag file.
|
||||
|
||||
What it does **not** remove:
|
||||
|
||||
- Skills installed via `npx skills add` — the `skills` CLI manages those. Run `npx skills remove caveman` (or use your IDE's skill manager).
|
||||
- Per-repo rule files written by `--with-init` (`.cursor/rules/`, `.windsurf/rules/`, `.clinerules/`, `.github/copilot-instructions.md`, `AGENTS.md`). Delete by hand if you want.
|
||||
- Per-repo rule files written by `--with-init` (`.cursor/rules/`, `.windsurf/rules/`, `.clinerules/`, `.github/copilot-instructions.md`, `.opencode/AGENTS.md`, `AGENTS.md`). Delete by hand if you want.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -186,8 +194,8 @@ Still broken? [Open an issue](https://github.com/JuliusBrussee/caveman/issues).
|
||||
**"I ran the installer but Claude Code isn't talking caveman."**
|
||||
|
||||
1. Run `node bin/install.js --list` — confirm `claude` is on the detected list. If not, `claude` isn't on `PATH`. Fix that first.
|
||||
2. Open `~/.claude/settings.json` and look for `"hooks"` containing `caveman-activate.js` and `caveman-mode-tracker.js`. If missing, re-run with `--force`.
|
||||
3. Check `~/.claude/.caveman-active` exists with content `full`. If not, the SessionStart hook silent-failed — check `~/.claude/hooks/` for the JS files and try `node ~/.claude/hooks/caveman-activate.js < /dev/null` to see if it errors.
|
||||
2. Open `$CLAUDE_CONFIG_DIR/settings.json` (default `~/.claude/settings.json`) and look for `"hooks"` containing `caveman-activate.js` and `caveman-mode-tracker.js`. If missing, re-run with `--force`.
|
||||
3. Check `$CLAUDE_CONFIG_DIR/.caveman-active` exists with content `full`. If not, the SessionStart hook silent-failed — check `$CLAUDE_CONFIG_DIR/hooks/` for the JS files and try `node $CLAUDE_CONFIG_DIR/hooks/caveman-activate.js < /dev/null` to see if it errors.
|
||||
4. Restart Claude Code. The SessionStart hook only fires on session start, not mid-session.
|
||||
|
||||
**"Hooks failing on Windows."**
|
||||
@@ -201,7 +209,7 @@ Still broken? [Open an issue](https://github.com/JuliusBrussee/caveman/issues).
|
||||
|
||||
The installer uses a JSONC-tolerant parser (`bin/lib/settings.js`) so comments and trailing commas don't crash the merge. It also runs `validateHookFields()` before every write so a malformed hook can't poison the file. If something still went wrong:
|
||||
|
||||
1. Check for a backup at `~/.claude/settings.json.bak` (installer writes one before any merge).
|
||||
1. Check for a backup at `$CLAUDE_CONFIG_DIR/settings.json.bak` (installer writes one before any merge).
|
||||
2. If no backup, restore from your shell history or version control.
|
||||
3. File an issue with the broken `settings.json` content (redacted) — that file passing validation but breaking Claude Code is a bug we want to fix.
|
||||
|
||||
@@ -225,13 +233,14 @@ The profile slug must exist in [vercel-labs/skills](https://github.com/vercel-la
|
||||
|
||||
## Privacy
|
||||
|
||||
The installer doesn't phone home. It only writes to:
|
||||
The installer doesn't phone home. It writes to:
|
||||
|
||||
- `$CLAUDE_CONFIG_DIR` (default `~/.claude/`) — hooks, flag file, settings merge.
|
||||
- The agent's own config location — Cursor's `.cursor/rules/`, Windsurf's `.windsurf/rules/`, etc.
|
||||
- `$CLAUDE_CONFIG_DIR` (default `~/.claude/`) — hooks, flag file, `settings.json` merge.
|
||||
- Each agent's own config location — Cursor's `.cursor/rules/`, Windsurf's `.windsurf/rules/`, opencode's `~/.config/opencode/`, etc.
|
||||
- Your current working directory (only with `--with-init`) — repo-local rule files.
|
||||
- `~/.openclaw/workspace/` (only with `--only openclaw` or `--with-init` when OpenClaw is detected) — the one `--with-init` side-effect outside the cwd.
|
||||
|
||||
No telemetry. No analytics. No network calls beyond `npm`/`npx` fetching the `skills` CLI from npm and (during install only) `git`/`curl` fetching this repo. Source: [`bin/install.js`](bin/install.js).
|
||||
No telemetry. No analytics. The installer's own code makes no network calls. Network requests do happen indirectly through the per-agent CLIs it shells out to — `claude plugin marketplace add`, `claude plugin install`, `gemini extensions install`, `npm view caveman-shrink`, and `npx -y skills add`. Each fetches from its own registry (Anthropic / GitHub / npm). Source: [`bin/install.js`](bin/install.js).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -84,11 +84,11 @@ One line. Find every agent. Install for each.
|
||||
# macOS / Linux / WSL / Git Bash
|
||||
curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash
|
||||
|
||||
# Windows (PowerShell)
|
||||
# Windows (PowerShell 5.1+)
|
||||
irm https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.ps1 | iex
|
||||
```
|
||||
|
||||
~30 seconds. Skip agent you no have. Safe to re-run.
|
||||
~30 seconds. Needs Node ≥18. Skip agent you no have. Safe to re-run.
|
||||
|
||||
**Trigger:** type `/caveman` or say "talk like caveman". Stop with "normal mode".
|
||||
|
||||
@@ -103,7 +103,7 @@ Install break? Open agent, say *"Read CLAUDE.md and INSTALL.md, install caveman
|
||||
| `/caveman-commit` | Conventional Commit messages, ≤50 char subject. Why over what. |
|
||||
| `/caveman-review` | One-line PR comments: `L42: 🔴 bug: user null. Add guard.` |
|
||||
| `/caveman-stats` | Real session token usage + lifetime savings + USD. Tweetable line via `--share`. |
|
||||
| `/caveman:compress <file>` | Rewrite memory file (e.g. `CLAUDE.md`) into caveman-speak. Cuts ~46% input tokens every session. Code/URLs/paths byte-preserved. |
|
||||
| `/caveman-compress <file>` | Rewrite memory file (e.g. `CLAUDE.md`) into caveman-speak. Cuts ~46% input tokens every session. Code/URLs/paths byte-preserved. |
|
||||
| `caveman-shrink` | MCP middleware. Wraps any MCP server, compresses tool descriptions. [npm](https://www.npmjs.com/package/caveman-shrink). |
|
||||
| `cavecrew-*` | Caveman subagents (investigator/builder/reviewer). ~60% fewer tokens than vanilla, main context lasts longer. |
|
||||
|
||||
@@ -163,9 +163,13 @@ Maintainer detail (hook architecture, file ownership, CI sync) live in [CLAUDE.m
|
||||
|
||||
[**OpenClaw**](https://openclaw.ai) the self-host gateway. One box, many agent inside (Claude Code, Codex, Pi, OpenCode), wired to your Slack / Discord / iMessage / Telegram / whatever. Tagline: *"The lobster way."* Lobster strong. Lobster smart. Lobster also talk a lot.
|
||||
|
||||
Caveman teach lobster brevity:
|
||||
Caveman teach lobster brevity — same canonical installer, scoped to one agent:
|
||||
|
||||
```bash
|
||||
# macOS / Linux / WSL
|
||||
curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash -s -- --only openclaw
|
||||
|
||||
# Windows (PowerShell): no Node? install Node ≥18 first, then
|
||||
npx -y github:JuliusBrussee/caveman -- --only openclaw
|
||||
```
|
||||
|
||||
|
||||
+129
-45
@@ -78,7 +78,7 @@ function parseArgs(argv) {
|
||||
case '--config-dir': {
|
||||
const v = argv[++i];
|
||||
if (!v || v.startsWith('--')) die('error: --config-dir requires a path');
|
||||
opts.configDir = v;
|
||||
opts.configDir = expandHome(v);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -90,6 +90,16 @@ function parseArgs(argv) {
|
||||
if (opts.minimal) { opts.withHooks = false; opts.withInit = false; opts.withMcpShrink = false; }
|
||||
if (opts.withHooks === 'auto') opts.withHooks = true;
|
||||
if (opts.withMcpShrink === 'auto') opts.withMcpShrink = true;
|
||||
// Validate --only ids against the provider matrix. PROVIDERS is defined later
|
||||
// in the file but is in scope by the time this function runs.
|
||||
if (opts.only.length) {
|
||||
const knownIds = new Set(PROVIDERS.map(p => p.id));
|
||||
for (const id of opts.only) {
|
||||
if (!knownIds.has(id)) {
|
||||
die(`error: unknown agent: ${id}\n see 'caveman --list' for valid ids`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
@@ -202,8 +212,6 @@ const PROVIDERS = [
|
||||
|
||||
// ── Detection ─────────────────────────────────────────────────────────────
|
||||
function hasCmd(cmd) {
|
||||
const which = process.platform === 'win32' ? 'where' : 'command';
|
||||
const args = process.platform === 'win32' ? [cmd] : ['-v', cmd];
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
const r = child_process.spawnSync('where', [cmd], { stdio: 'ignore' });
|
||||
@@ -212,8 +220,6 @@ function hasCmd(cmd) {
|
||||
const r = child_process.spawnSync('sh', ['-c', `command -v ${shellEscape(cmd)}`], { stdio: 'ignore' });
|
||||
return r.status === 0;
|
||||
} catch (_) { return false; }
|
||||
// unreachable; satisfy linters
|
||||
void which; void args;
|
||||
}
|
||||
|
||||
function shellEscape(s) { return `'${String(s).replace(/'/g, `'\\''`)}'`; }
|
||||
@@ -369,7 +375,7 @@ function absoluteNodePath() {
|
||||
}
|
||||
|
||||
// ── Per-provider installers ────────────────────────────────────────────────
|
||||
function installClaude(ctx) {
|
||||
async function installClaude(ctx) {
|
||||
const { say, note, warn, ok, opts, results } = ctx;
|
||||
results.detected++;
|
||||
say('→ Claude Code detected');
|
||||
@@ -392,7 +398,7 @@ function installClaude(ctx) {
|
||||
|
||||
if (opts.withHooks) {
|
||||
say(' → installing hooks (--with-hooks)');
|
||||
const r = installHooks(ctx);
|
||||
const r = await installHooks(ctx);
|
||||
if (r === 'ok') results.installed.push('claude-hooks');
|
||||
else if (r === 'skip') results.skipped.push(['claude-hooks', 'already wired']);
|
||||
else results.failed.push(['claude-hooks', r]);
|
||||
@@ -450,6 +456,11 @@ const OPENCODE_AGENT_FILES = ['cavecrew-investigator.md', 'cavecrew-builder.md',
|
||||
const OPENCODE_COMMAND_FILES = ['caveman.md', 'caveman-commit.md', 'caveman-review.md', 'caveman-compress.md', 'caveman-stats.md', 'caveman-help.md'];
|
||||
const OPENCODE_PLUGIN_REL = './plugins/caveman/plugin.js';
|
||||
const OPENCODE_AGENTS_MD_SENTINEL = 'Respond terse like smart caveman';
|
||||
// Marker fence for the opencode AGENTS.md ruleset block. Same convention as
|
||||
// bin/lib/openclaw.js for SOUL.md — lets us strip our block cleanly even when
|
||||
// the user has authored content above AND below it.
|
||||
const OPENCODE_AGENTS_MD_BEGIN = '<!-- caveman-begin -->';
|
||||
const OPENCODE_AGENTS_MD_END = '<!-- caveman-end -->';
|
||||
|
||||
function opencodeConfigDir() {
|
||||
if (process.env.XDG_CONFIG_HOME) return path.join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
@@ -503,16 +514,25 @@ function installOpencode(ctx) {
|
||||
|
||||
try {
|
||||
// 1. Plugin dir — copy plugin.js, package.json, caveman-config.js (sibling).
|
||||
// Same `--force` semantic as commands/agents/skills below: re-runs leave
|
||||
// user edits to plugin.js alone unless --force is passed.
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
const pluginSrc = path.join(repoRoot, 'src', 'plugins', 'opencode');
|
||||
fs.copyFileSync(path.join(pluginSrc, 'plugin.js'), path.join(pluginDir, 'plugin.js'));
|
||||
fs.copyFileSync(path.join(pluginSrc, 'package.json'), path.join(pluginDir, 'package.json'));
|
||||
// Renamed to .cjs because the plugin dir is "type": "module" — a bare .js
|
||||
// sibling would be loaded as ESM and break the plugin's require() bridge.
|
||||
fs.copyFileSync(
|
||||
path.join(repoRoot, 'src', 'hooks', 'caveman-config.js'),
|
||||
path.join(pluginDir, 'caveman-config.cjs'),
|
||||
);
|
||||
const pluginPayload = [
|
||||
[path.join(pluginSrc, 'plugin.js'), path.join(pluginDir, 'plugin.js')],
|
||||
[path.join(pluginSrc, 'package.json'), path.join(pluginDir, 'package.json')],
|
||||
// Renamed to .cjs because the plugin dir is "type": "module" — a bare .js
|
||||
// sibling would be loaded as ESM and break the plugin's require() bridge.
|
||||
[path.join(repoRoot, 'src', 'hooks', 'caveman-config.js'),
|
||||
path.join(pluginDir, 'caveman-config.cjs')],
|
||||
];
|
||||
for (const [src, dest] of pluginPayload) {
|
||||
if (fs.existsSync(dest) && !opts.force) {
|
||||
note(` skipped ${dest} (exists; --force to overwrite)`);
|
||||
continue;
|
||||
}
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
process.stdout.write(` installed: ${pluginDir}\n`);
|
||||
|
||||
// 2. Commands.
|
||||
@@ -550,20 +570,36 @@ function installOpencode(ctx) {
|
||||
process.stdout.write(` installed: ${dest}/\n`);
|
||||
}
|
||||
|
||||
// 5. AGENTS.md — Tier-3 always-on ruleset. Append-with-sentinel so we
|
||||
// don't clobber a user-authored AGENTS.md.
|
||||
// 5. AGENTS.md — Tier-3 always-on ruleset. Wrapped in begin/end markers so
|
||||
// a later --uninstall can strip our block cleanly even if the user has
|
||||
// authored content above AND below it. Idempotency check uses the begin
|
||||
// marker (the legacy sentinel still matches old installs).
|
||||
const ruleBody = fs.readFileSync(path.join(repoRoot, 'src', 'rules', 'caveman-activate.md'), 'utf8').trimEnd() + '\n';
|
||||
const fencedBlock = `${OPENCODE_AGENTS_MD_BEGIN}\n${ruleBody}${OPENCODE_AGENTS_MD_END}\n`;
|
||||
if (fs.existsSync(agentsMd)) {
|
||||
const existing = fs.readFileSync(agentsMd, 'utf8');
|
||||
if (!existing.includes(OPENCODE_AGENTS_MD_SENTINEL)) {
|
||||
const sep = existing.endsWith('\n\n') ? '' : (existing.endsWith('\n') ? '\n' : '\n\n');
|
||||
fs.writeFileSync(agentsMd, existing + sep + ruleBody, { mode: 0o644 });
|
||||
process.stdout.write(` appended caveman ruleset to ${agentsMd}\n`);
|
||||
} else {
|
||||
const alreadyFenced = existing.includes(OPENCODE_AGENTS_MD_BEGIN)
|
||||
&& existing.includes(OPENCODE_AGENTS_MD_END);
|
||||
const alreadyByLegacySentinel = !alreadyFenced && existing.includes(OPENCODE_AGENTS_MD_SENTINEL);
|
||||
if (alreadyFenced) {
|
||||
note(` ${agentsMd} already contains caveman ruleset`);
|
||||
} else if (alreadyByLegacySentinel) {
|
||||
note(` ${agentsMd} contains a legacy (un-fenced) caveman block — leaving as-is`);
|
||||
note(' re-run with --force to replace it with a fenced block');
|
||||
if (opts.force) {
|
||||
// Replace the entire file with a clean fenced version. The legacy
|
||||
// path didn't fence, so we can't isolate the block — full rewrite is
|
||||
// the only safe option under --force.
|
||||
fs.writeFileSync(agentsMd, fencedBlock, { mode: 0o644 });
|
||||
process.stdout.write(` rewrote ${agentsMd} with fenced caveman block\n`);
|
||||
}
|
||||
} else {
|
||||
const sep = existing.endsWith('\n\n') ? '' : (existing.endsWith('\n') ? '\n' : '\n\n');
|
||||
fs.writeFileSync(agentsMd, existing + sep + fencedBlock, { mode: 0o644 });
|
||||
process.stdout.write(` appended caveman ruleset to ${agentsMd}\n`);
|
||||
}
|
||||
} else {
|
||||
fs.writeFileSync(agentsMd, ruleBody, { mode: 0o644 });
|
||||
fs.writeFileSync(agentsMd, fencedBlock, { mode: 0o644 });
|
||||
process.stdout.write(` installed: ${agentsMd}\n`);
|
||||
}
|
||||
|
||||
@@ -575,8 +611,11 @@ function installOpencode(ctx) {
|
||||
process.stdout.write('\n');
|
||||
return;
|
||||
}
|
||||
if (fs.existsSync(opencodeJson)) {
|
||||
try { fs.copyFileSync(opencodeJson, opencodeJson + '.bak'); } catch (_) {}
|
||||
// Preserve the original on first install only — repeat installs would
|
||||
// otherwise overwrite the only known-good copy with an already-merged file.
|
||||
const opencodeBak = opencodeJson + '.bak';
|
||||
if (fs.existsSync(opencodeJson) && !fs.existsSync(opencodeBak)) {
|
||||
try { fs.copyFileSync(opencodeJson, opencodeBak); } catch (_) {}
|
||||
}
|
||||
if (!Array.isArray(cfg.plugin)) cfg.plugin = [];
|
||||
if (!cfg.plugin.includes(OPENCODE_PLUGIN_REL)) {
|
||||
@@ -637,7 +676,7 @@ function installOpenclaw(ctx) {
|
||||
|
||||
// ── Hooks installer ────────────────────────────────────────────────────────
|
||||
// Replaces src/hooks/install.sh + src/hooks/install.ps1.
|
||||
function installHooks(ctx) {
|
||||
async function installHooks(ctx) {
|
||||
const { note, warn, opts, repoRoot, configDir } = ctx;
|
||||
const hooksDir = path.join(configDir, 'hooks');
|
||||
const settingsPath = path.join(configDir, 'settings.json');
|
||||
@@ -658,7 +697,7 @@ function installHooks(ctx) {
|
||||
if (sourceDir && fs.existsSync(path.join(sourceDir, f))) {
|
||||
fs.copyFileSync(path.join(sourceDir, f), dest);
|
||||
} else {
|
||||
try { downloadTo(`${HOOKS_REMOTE}/${f}`, dest); }
|
||||
try { await downloadTo(`${HOOKS_REMOTE}/${f}`, dest); }
|
||||
catch (e) { return `download ${f} failed: ${e.message}`; }
|
||||
}
|
||||
process.stdout.write(` installed: ${dest}\n`);
|
||||
@@ -673,9 +712,12 @@ function installHooks(ctx) {
|
||||
warn(' settings.json unparseable; will not touch it. Edit manually then re-run.');
|
||||
return 'settings.json unparseable';
|
||||
}
|
||||
// Backup once per install run
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
try { fs.copyFileSync(settingsPath, settingsPath + '.bak'); } catch (_) {}
|
||||
// Backup once, preserved across reinstalls. Without the !fs.existsSync(bak)
|
||||
// guard, the second install would overwrite the only known-good copy with
|
||||
// the already-merged file, destroying recovery.
|
||||
const bak = settingsPath + '.bak';
|
||||
if (fs.existsSync(settingsPath) && !fs.existsSync(bak)) {
|
||||
try { fs.copyFileSync(settingsPath, bak); } catch (_) {}
|
||||
}
|
||||
|
||||
const node = absoluteNodePath();
|
||||
@@ -758,7 +800,7 @@ function installMcpShrink(ctx) {
|
||||
}
|
||||
|
||||
// ── Init writers (per-repo rule files) ────────────────────────────────────
|
||||
function runInit(ctx) {
|
||||
async function runInit(ctx) {
|
||||
const { note, warn, opts, repoRoot } = ctx;
|
||||
const local = repoRoot && path.join(repoRoot, 'src/tools/caveman-init.js');
|
||||
const args = [process.cwd()];
|
||||
@@ -775,7 +817,7 @@ function runInit(ctx) {
|
||||
}
|
||||
try {
|
||||
const tmp = path.join(os.tmpdir(), `caveman-init-${process.pid}.js`);
|
||||
downloadTo(INIT_SCRIPT_URL, tmp);
|
||||
await downloadTo(INIT_SCRIPT_URL, tmp);
|
||||
const r = child_process.spawnSync(absoluteNodePath(), [tmp, ...args], { stdio: 'inherit' });
|
||||
try { fs.unlinkSync(tmp); } catch (_) {}
|
||||
return (r.status || 0) === 0;
|
||||
@@ -846,15 +888,34 @@ function uninstall(ctx) {
|
||||
// Don't rmdir hooksDir — other plugins may use it.
|
||||
}
|
||||
|
||||
// Plugin uninstall on Claude
|
||||
// Plugin uninstall on Claude. Probe `plugin list` first so a re-run on a
|
||||
// machine where caveman was never installed (or was already removed) doesn't
|
||||
// print "Plugin not installed" stderr noise.
|
||||
if (hasCmd('claude')) {
|
||||
const r = runSpawn('claude', ['plugin', 'uninstall', 'caveman@caveman'], null, opts.dryRun);
|
||||
if ((r.status || 0) === 0) ok(' removed claude plugin');
|
||||
const probe = captureSpawn('claude', ['plugin', 'list']);
|
||||
if (probe.status === 0 && /caveman/i.test(probe.stdout || '')) {
|
||||
const r = runSpawn('claude', ['plugin', 'uninstall', 'caveman@caveman'], null, opts.dryRun);
|
||||
if ((r.status || 0) === 0) ok(' removed claude plugin');
|
||||
} else {
|
||||
note(' claude plugin not installed — skipping');
|
||||
}
|
||||
|
||||
// caveman-shrink MCP — only run if `claude mcp` subcommand exists. Tolerate
|
||||
// non-zero exit (server may have never been registered).
|
||||
const mcpHelp = captureSpawn('claude', ['mcp', '--help']);
|
||||
if (mcpHelp.status === 0) {
|
||||
runSpawn('claude', ['mcp', 'remove', 'caveman-shrink'], null, opts.dryRun);
|
||||
}
|
||||
}
|
||||
|
||||
// Gemini extension
|
||||
// Gemini extension. Same idempotency probe as claude.
|
||||
if (hasCmd('gemini')) {
|
||||
runSpawn('gemini', ['extensions', 'uninstall', 'caveman'], null, opts.dryRun);
|
||||
const probe = captureSpawn('gemini', ['extensions', 'list']);
|
||||
if (probe.status === 0 && /caveman/i.test(probe.stdout || '')) {
|
||||
runSpawn('gemini', ['extensions', 'uninstall', 'caveman'], null, opts.dryRun);
|
||||
} else {
|
||||
note(' gemini extension not installed — skipping');
|
||||
}
|
||||
}
|
||||
|
||||
// opencode native install — strip plugin entry, MCP entry, and our files.
|
||||
@@ -894,17 +955,35 @@ function uninstall(ctx) {
|
||||
const p = path.join(ocDir, 'skills', name);
|
||||
if (fs.existsSync(p) && !opts.dryRun) { try { fs.rmSync(p, { recursive: true, force: true }); } catch (_) {} }
|
||||
}
|
||||
// AGENTS.md — only remove if our sentinel is the entire content (we
|
||||
// wrote it) or strip our block from a mixed file.
|
||||
// AGENTS.md — strip the fenced caveman block (preserves user content
|
||||
// above and below). If the file is empty after the strip, remove it.
|
||||
// Falls back to legacy unfenced-sentinel handling for installs that
|
||||
// pre-date the marker fence.
|
||||
const ocAgentsMd = path.join(ocDir, 'AGENTS.md');
|
||||
if (fs.existsSync(ocAgentsMd)) {
|
||||
const body = fs.readFileSync(ocAgentsMd, 'utf8');
|
||||
if (body.includes(OPENCODE_AGENTS_MD_SENTINEL)) {
|
||||
const begin = body.indexOf(OPENCODE_AGENTS_MD_BEGIN);
|
||||
const end = body.indexOf(OPENCODE_AGENTS_MD_END);
|
||||
if (begin !== -1 && end !== -1 && end > begin) {
|
||||
const before = body.slice(0, begin).replace(/\n+$/, '\n');
|
||||
const after = body.slice(end + OPENCODE_AGENTS_MD_END.length).replace(/^\n+/, '\n');
|
||||
let next = (before + after).trimEnd();
|
||||
next = next ? next + '\n' : '';
|
||||
if (!opts.dryRun) {
|
||||
if (next === '') {
|
||||
try { fs.unlinkSync(ocAgentsMd); } catch (_) {}
|
||||
} else {
|
||||
fs.writeFileSync(ocAgentsMd, next, { mode: 0o644 });
|
||||
}
|
||||
}
|
||||
note(next === '' ? ` removed ${ocAgentsMd}` : ` stripped caveman block from ${ocAgentsMd}`);
|
||||
} else if (body.includes(OPENCODE_AGENTS_MD_SENTINEL)) {
|
||||
// Legacy install (no marker fence). Remove only if the file is ours.
|
||||
if (body.trim() === '' || body.trim().startsWith(OPENCODE_AGENTS_MD_SENTINEL)) {
|
||||
if (!opts.dryRun) { try { fs.unlinkSync(ocAgentsMd); } catch (_) {} }
|
||||
note(` removed ${ocAgentsMd}`);
|
||||
} else {
|
||||
note(` left ${ocAgentsMd} in place (mixed content — strip caveman block manually)`);
|
||||
note(` left ${ocAgentsMd} in place (legacy mixed content — strip caveman block manually)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -985,6 +1064,7 @@ FLAGS
|
||||
--dry-run Print what would run, do nothing.
|
||||
--force Re-run even if a target reports already installed.
|
||||
--only <agent> Install only for the named agent. Repeatable.
|
||||
See --list for valid ids.
|
||||
--skip-skills Don't run the npx-skills auto-detect fallback.
|
||||
--all Turn on hooks + init + mcp-shrink.
|
||||
--minimal Just the plugin/extension install.
|
||||
@@ -995,7 +1075,11 @@ FLAGS
|
||||
--with-mcp-shrink Claude Code: register caveman-shrink MCP proxy. (Default ON.)
|
||||
--no-mcp-shrink Skip MCP shrink.
|
||||
--uninstall, -u Remove caveman from this machine.
|
||||
--config-dir <path> Use this dir as Claude config dir (default: \$CLAUDE_CONFIG_DIR or ~/.claude).
|
||||
--config-dir <path> Claude Code config dir for hook files + settings.json.
|
||||
Default: \$CLAUDE_CONFIG_DIR or ~/.claude. Does NOT
|
||||
scope \`claude plugin install\`, \`gemini extensions
|
||||
install\`, opencode (XDG_CONFIG_HOME), or openclaw
|
||||
(OPENCLAW_WORKSPACE) — those use their own paths.
|
||||
--non-interactive Never prompt; use defaults. (Auto when stdin is not a TTY.)
|
||||
--list Print provider matrix and exit.
|
||||
--no-color Disable ANSI colors.
|
||||
@@ -1065,7 +1149,7 @@ async function main() {
|
||||
// no repo clone is available; openclaw bails when the workspace dir is
|
||||
// missing without --force).
|
||||
if (!explicit(prov.id) && !detectMatch(prov.detect)) continue;
|
||||
if (prov.id === 'claude') { installClaude(ctx); continue; }
|
||||
if (prov.id === 'claude') { await installClaude(ctx); continue; }
|
||||
if (prov.id === 'gemini') { installGemini(ctx); continue; }
|
||||
if (prov.id === 'opencode') { installOpencode(ctx); continue; }
|
||||
if (prov.id === 'openclaw') { installOpenclaw(ctx); continue; }
|
||||
@@ -1084,8 +1168,8 @@ async function main() {
|
||||
// Per-repo init
|
||||
if (opts.withInit) {
|
||||
ctx.say(`→ writing per-repo IDE rule files into ${process.cwd()} (--with-init)`);
|
||||
if (runInit(ctx)) ctx.results.installed.push(`caveman-init (${process.cwd()})`);
|
||||
else ctx.results.failed.push(['caveman-init', 'src/tools/caveman-init.js failed']);
|
||||
if (await runInit(ctx)) ctx.results.installed.push(`caveman-init (${process.cwd()})`);
|
||||
else ctx.results.failed.push(['caveman-init', 'src/tools/caveman-init.js failed']);
|
||||
process.stdout.write('\n');
|
||||
} else if (ctx.results.installed.length || ctx.results.skipped.length) {
|
||||
ctx.note(' tip: re-run inside a repo with --all (or --with-init) to also write per-repo');
|
||||
|
||||
@@ -151,10 +151,16 @@ function addCommandHook(settings, event, opts) {
|
||||
|
||||
// ── removeCavemanHooks ────────────────────────────────────────────────────
|
||||
// Strip every entry whose any hook command mentions `marker`. Empties events.
|
||||
// Tolerates malformed pre-existing settings (non-array hook lists, foreign
|
||||
// shapes) — those get dropped by validateHookFields first so we never call
|
||||
// .length / .filter on a non-array.
|
||||
function removeCavemanHooks(settings, marker = 'caveman') {
|
||||
if (!settings || !settings.hooks) return 0;
|
||||
validateHookFields(settings);
|
||||
if (!settings.hooks) return 0; // validate may have deleted the whole tree
|
||||
let removed = 0;
|
||||
for (const ev of Object.keys(settings.hooks)) {
|
||||
if (!Array.isArray(settings.hooks[ev])) { delete settings.hooks[ev]; continue; }
|
||||
const before = settings.hooks[ev].length;
|
||||
settings.hooks[ev] = settings.hooks[ev].filter(entry => {
|
||||
if (!entry || !Array.isArray(entry.hooks)) return true;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Windows install fallback
|
||||
|
||||
If `irm https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.ps1 | iex` fails on Windows (issues #249, #199, #72), set up plugin-skill activation by hand. This does **not** install the standalone hooks or the statusline — for those, run `hooks/install.ps1` after this.
|
||||
If `irm https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.ps1 | iex` fails on Windows (issues #249, #199, #72), set up plugin-skill activation by hand. This does **not** install the standalone hooks or the statusline — for those, run the unified Node installer afterwards: `npx -y github:JuliusBrussee/caveman -- --only claude` (or `node bin/install.js --only claude` from a clone).
|
||||
|
||||
```powershell
|
||||
$ClaudeDir = if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { Join-Path $HOME ".claude" }
|
||||
@@ -8,7 +8,7 @@ $PluginSkillDir = Join-Path $ClaudeDir ".agents\plugins\caveman\skills\caveman"
|
||||
$MarketplaceDir = Join-Path $ClaudeDir ".agents\plugins"
|
||||
$MarketplaceFile = Join-Path $MarketplaceDir "marketplace.json"
|
||||
|
||||
# Copy SKILL.md into the plugin path
|
||||
# Copy SKILL.md into the plugin path (run from a clone of the repo)
|
||||
New-Item -ItemType Directory -Path $PluginSkillDir -Force | Out-Null
|
||||
Copy-Item ".\skills\caveman\SKILL.md" "$PluginSkillDir\SKILL.md" -Force
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ Claude read `CLAUDE.md` on every session start. If file big, cost big. Caveman m
|
||||
## What It Do
|
||||
|
||||
```
|
||||
/caveman:compress CLAUDE.md
|
||||
/caveman-compress CLAUDE.md
|
||||
```
|
||||
|
||||
```
|
||||
@@ -71,7 +71,7 @@ All validations passed ✅ — headings, code blocks, URLs, file paths preserved
|
||||
|
||||
## Install
|
||||
|
||||
Compress is built in with the `caveman` plugin. Install `caveman` once, then use `/caveman:compress`.
|
||||
Compress is built in with the `caveman` plugin. Install `caveman` once, then use `/caveman-compress`.
|
||||
|
||||
If you need local files, the compress skill lives at:
|
||||
|
||||
@@ -84,14 +84,14 @@ caveman-compress/
|
||||
## Usage
|
||||
|
||||
```
|
||||
/caveman:compress <filepath>
|
||||
/caveman-compress <filepath>
|
||||
```
|
||||
|
||||
Examples:
|
||||
```
|
||||
/caveman:compress CLAUDE.md
|
||||
/caveman:compress docs/preferences.md
|
||||
/caveman:compress todos.md
|
||||
/caveman-compress CLAUDE.md
|
||||
/caveman-compress docs/preferences.md
|
||||
/caveman-compress todos.md
|
||||
```
|
||||
|
||||
### What files work
|
||||
@@ -106,7 +106,7 @@ Examples:
|
||||
## How It Work
|
||||
|
||||
```
|
||||
/caveman:compress CLAUDE.md
|
||||
/caveman-compress CLAUDE.md
|
||||
↓
|
||||
detect file type (no tokens)
|
||||
↓
|
||||
|
||||
@@ -29,7 +29,7 @@ Mode stick until changed or session end.
|
||||
|-------|---------|-----------|
|
||||
| **caveman-commit** | `/caveman-commit` | Terse commit messages. Conventional Commits. ≤50 char subject. |
|
||||
| **caveman-review** | `/caveman-review` | One-line PR comments: `L42: bug: user null. Add guard.` |
|
||||
| **caveman-compress** | `/caveman:compress <file>` | Compress .md files to caveman prose. Saves ~46% input tokens. |
|
||||
| **caveman-compress** | `/caveman-compress <file>` | Compress .md files to caveman prose. Saves ~46% input tokens. |
|
||||
| **caveman-help** | `/caveman-help` | This card. |
|
||||
|
||||
## Deactivate
|
||||
|
||||
+18
-14
@@ -2,27 +2,29 @@
|
||||
|
||||
These hooks are **bundled with the caveman plugin** and activate automatically when the plugin is installed. No manual setup required.
|
||||
|
||||
If you installed caveman standalone (without the plugin), you can use `bash hooks/install.sh` to wire them into your settings.json manually.
|
||||
If you installed caveman standalone (without the plugin), the unified Node installer at `bin/install.js` wires them into your `settings.json` for you — run `node bin/install.js --only claude` from a clone, or `npx -y github:JuliusBrussee/caveman -- --only claude` for the curl-pipe path.
|
||||
|
||||
## What's Included
|
||||
|
||||
### `caveman-activate.js` — SessionStart hook
|
||||
|
||||
- Runs once when Claude Code starts
|
||||
- Writes `full` to `~/.claude/.caveman-active` (flag file)
|
||||
- Writes `full` to `$CLAUDE_CONFIG_DIR/.caveman-active` (default `~/.claude/.caveman-active`) via the symlink-safe `safeWriteFlag` helper
|
||||
- Emits caveman rules as hidden SessionStart context
|
||||
- Detects missing statusline config and emits setup nudge (Claude will offer to help)
|
||||
|
||||
### `caveman-mode-tracker.js` — UserPromptSubmit hook
|
||||
|
||||
- Fires on every user prompt, checks for `/caveman` commands
|
||||
- Writes the active mode to the flag file when a caveman command is detected
|
||||
- Supports: `full`, `lite`, `ultra`, `wenyan`, `wenyan-lite`, `wenyan-ultra`, `commit`, `review`, `compress`
|
||||
- Fires on every user prompt, checks for `/caveman` commands and natural-language activation/deactivation phrases ("talk like caveman", "stop caveman", "normal mode")
|
||||
- Writes the active mode to the flag file when a caveman command is detected; deletes it on deactivation
|
||||
- Emits a small per-turn reinforcement reminder when the flag is set to a non-independent mode (`lite`/`full`/`ultra`/`wenyan*`)
|
||||
- Supports: `lite`, `full`, `ultra`, `wenyan`, `wenyan-lite`, `wenyan-full`, `wenyan-ultra`, `commit`, `review`, `compress`
|
||||
|
||||
### `caveman-statusline.sh` / `caveman-statusline.ps1` — Statusline badge script
|
||||
|
||||
- Reads `~/.claude/.caveman-active` and outputs a colored badge
|
||||
- Reads `$CLAUDE_CONFIG_DIR/.caveman-active` (default `~/.claude/.caveman-active`) and outputs a colored badge
|
||||
- Shows `[CAVEMAN]`, `[CAVEMAN:ULTRA]`, `[CAVEMAN:WENYAN]`, etc.
|
||||
- Appends the lifetime savings suffix `⛏ 12.4k` from `$CLAUDE_CONFIG_DIR/.caveman-statusline-suffix` (written by `caveman-stats.js` on each `/caveman-stats` run; absent until the first run, so fresh installs render no fake number). Opt out with `CAVEMAN_STATUSLINE_SAVINGS=0`.
|
||||
|
||||
## Statusline Badge
|
||||
|
||||
@@ -32,7 +34,7 @@ The statusline badge shows which caveman mode is active directly in your Claude
|
||||
|
||||
If you already have a custom statusline, caveman does not overwrite it and Claude stays quiet. Add the badge snippet to your existing script instead.
|
||||
|
||||
**Standalone users:** `install.sh` / `install.ps1` wires the statusline automatically if you do not already have a custom statusline. If you do, the installer leaves it alone and prints the merge note.
|
||||
**Standalone users:** the unified installer (`bin/install.js`, invoked by the `install.sh` / `install.ps1` shims at the repo root) wires the statusline automatically if you do not already have a custom statusline. If you do, the installer leaves it alone and prints the merge note.
|
||||
|
||||
**Manual setup:** If you need to configure it yourself, add one of these to `~/.claude/settings.json`:
|
||||
|
||||
@@ -60,7 +62,7 @@ Replace the path with the actual script location (e.g. `~/.claude/hooks/` for st
|
||||
|
||||
```bash
|
||||
caveman_text=""
|
||||
caveman_flag="$HOME/.claude/.caveman-active"
|
||||
caveman_flag="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.caveman-active"
|
||||
if [ -f "$caveman_flag" ]; then
|
||||
caveman_mode=$(cat "$caveman_flag" 2>/dev/null)
|
||||
if [ "$caveman_mode" = "full" ] || [ -z "$caveman_mode" ]; then
|
||||
@@ -82,7 +84,7 @@ Badge examples:
|
||||
## How It Works
|
||||
|
||||
```
|
||||
SessionStart hook ──writes "full"──▶ ~/.claude/.caveman-active ◀──writes mode── UserPromptSubmit hook
|
||||
SessionStart hook ──writes "full"──▶ $CLAUDE_CONFIG_DIR/.caveman-active ◀──writes mode── UserPromptSubmit hook
|
||||
│
|
||||
reads
|
||||
▼
|
||||
@@ -96,12 +98,14 @@ SessionStart stdout is injected as hidden system context — Claude sees it, use
|
||||
|
||||
If installed via plugin: disable the plugin — hooks deactivate automatically.
|
||||
|
||||
If installed via `install.sh`:
|
||||
If installed via the standalone Node installer:
|
||||
```bash
|
||||
bash hooks/uninstall.sh
|
||||
npx -y github:JuliusBrussee/caveman -- --uninstall
|
||||
# or, from a clone:
|
||||
node bin/install.js --uninstall
|
||||
```
|
||||
|
||||
Or manually:
|
||||
1. Remove `~/.claude/hooks/caveman-activate.js`, `~/.claude/hooks/caveman-mode-tracker.js`, and the matching statusline script (`caveman-statusline.sh` on macOS/Linux or `caveman-statusline.ps1` on Windows)
|
||||
2. Remove the SessionStart, UserPromptSubmit, and statusLine entries from `~/.claude/settings.json`
|
||||
3. Delete `~/.claude/.caveman-active`
|
||||
1. Remove the caveman hook files from `$CLAUDE_CONFIG_DIR/hooks/` (default `~/.claude/hooks/`): `caveman-activate.js`, `caveman-mode-tracker.js`, `caveman-stats.js`, `caveman-config.js`, and `caveman-statusline.{sh,ps1}`.
|
||||
2. Remove the SessionStart, UserPromptSubmit, and statusLine entries from `$CLAUDE_CONFIG_DIR/settings.json`.
|
||||
3. Delete `$CLAUDE_CONFIG_DIR/.caveman-active` (and `$CLAUDE_CONFIG_DIR/.caveman-statusline-suffix` if you ran `/caveman-stats`).
|
||||
|
||||
@@ -79,7 +79,12 @@ test('opencode fresh install drops plugin, commands, agents, skills, AGENTS.md,
|
||||
assert.ok(fs.existsSync(path.join(ocDir, 'skills', name, 'SKILL.md')), `skill ${name}/SKILL.md missing`);
|
||||
}
|
||||
assert.ok(fs.existsSync(path.join(ocDir, 'AGENTS.md')), 'AGENTS.md missing');
|
||||
assert.match(fs.readFileSync(path.join(ocDir, 'AGENTS.md'), 'utf8'), /Respond terse like smart caveman/);
|
||||
const agentsBody = fs.readFileSync(path.join(ocDir, 'AGENTS.md'), 'utf8');
|
||||
assert.match(agentsBody, /Respond terse like smart caveman/);
|
||||
// Block must be wrapped in begin/end markers so uninstall can isolate it
|
||||
// from user-authored content above and below.
|
||||
assert.match(agentsBody, /<!-- caveman-begin -->/);
|
||||
assert.match(agentsBody, /<!-- caveman-end -->/);
|
||||
|
||||
const cfgPath = path.join(ocDir, 'opencode.json');
|
||||
assert.ok(fs.existsSync(cfgPath), 'opencode.json missing');
|
||||
@@ -117,6 +122,71 @@ test('opencode idempotent install does not duplicate plugin entries', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── 2b. Plugin payload not overwritten on re-install (without --force) ────
|
||||
test('opencode re-install preserves user edits to plugin.js without --force', () => {
|
||||
const xdg = freshTmpDir();
|
||||
const shimDir = shimOpencode();
|
||||
try {
|
||||
const env = { ...process.env, XDG_CONFIG_HOME: xdg, PATH: pathWith(shimDir), NO_COLOR: '1' };
|
||||
const r1 = runInstaller(['--only', 'opencode'], env);
|
||||
assert.notEqual(r1.status, 2);
|
||||
|
||||
const pluginPath = path.join(xdg, 'opencode', 'plugins', 'caveman', 'plugin.js');
|
||||
const tweak = '\n// USER-TWEAK-DO-NOT-OVERWRITE\n';
|
||||
fs.appendFileSync(pluginPath, tweak);
|
||||
const beforeBytes = fs.readFileSync(pluginPath, 'utf8');
|
||||
|
||||
const r2 = runInstaller(['--only', 'opencode'], env);
|
||||
assert.notEqual(r2.status, 2);
|
||||
|
||||
const afterBytes = fs.readFileSync(pluginPath, 'utf8');
|
||||
assert.equal(afterBytes, beforeBytes, 'second install should not overwrite plugin.js without --force');
|
||||
assert.match(afterBytes, /USER-TWEAK-DO-NOT-OVERWRITE/);
|
||||
|
||||
// With --force, the file should be replaced (no tweak afterward).
|
||||
const r3 = runInstaller(['--only', 'opencode', '--force'], env);
|
||||
assert.notEqual(r3.status, 2);
|
||||
const forced = fs.readFileSync(pluginPath, 'utf8');
|
||||
assert.doesNotMatch(forced, /USER-TWEAK-DO-NOT-OVERWRITE/, '--force should overwrite plugin.js');
|
||||
} finally {
|
||||
fs.rmSync(xdg, { recursive: true, force: true });
|
||||
fs.rmSync(shimDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ── 2c. AGENTS.md fence preserves user content above and below ───────────
|
||||
test('opencode uninstall strips fenced AGENTS.md block, preserving user prefix and suffix', () => {
|
||||
const xdg = freshTmpDir();
|
||||
const shimDir = shimOpencode();
|
||||
try {
|
||||
const env = { ...process.env, XDG_CONFIG_HOME: xdg, PATH: pathWith(shimDir), NO_COLOR: '1' };
|
||||
const r1 = runInstaller(['--only', 'opencode'], env);
|
||||
assert.notEqual(r1.status, 2);
|
||||
|
||||
const agentsMd = path.join(xdg, 'opencode', 'AGENTS.md');
|
||||
const installed = fs.readFileSync(agentsMd, 'utf8');
|
||||
// Sandwich the caveman block between user prefix and suffix.
|
||||
const userPrefix = '# my project\n\nuse 2-space indent.\n\n';
|
||||
const userSuffix = '\n## extra\n\nkeep PRs small.\n';
|
||||
fs.writeFileSync(agentsMd, userPrefix + installed.trimEnd() + '\n' + userSuffix);
|
||||
|
||||
const r2 = runInstaller(['--uninstall'], env);
|
||||
assert.notEqual(r2.status, 2);
|
||||
|
||||
const after = fs.readFileSync(agentsMd, 'utf8');
|
||||
assert.doesNotMatch(after, /<!-- caveman-begin -->/, 'caveman block should be stripped');
|
||||
assert.doesNotMatch(after, /<!-- caveman-end -->/, 'caveman end marker should be stripped');
|
||||
assert.doesNotMatch(after, /Respond terse like smart caveman/, 'caveman body should be stripped');
|
||||
assert.match(after, /# my project/, 'user prefix should survive');
|
||||
assert.match(after, /use 2-space indent/, 'user prefix body should survive');
|
||||
assert.match(after, /## extra/, 'user suffix should survive');
|
||||
assert.match(after, /keep PRs small/, 'user suffix body should survive');
|
||||
} finally {
|
||||
fs.rmSync(xdg, { recursive: true, force: true });
|
||||
fs.rmSync(shimDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ── 3. Tolerates JSONC opencode.json (#249-class regression guard) ───────
|
||||
test('opencode install tolerates JSONC opencode.json (comments + trailing commas)', () => {
|
||||
const xdg = freshTmpDir();
|
||||
|
||||
@@ -68,3 +68,44 @@ test('aider alias rewrites to aider-desk in dry-run output', () => {
|
||||
// No detection means no install lines, but the script should not crash.
|
||||
assert.equal(r.status, 0);
|
||||
});
|
||||
|
||||
test('--only with unknown agent id exits 2', () => {
|
||||
const r = run('--only', 'definitely-not-an-agent', '--non-interactive');
|
||||
assert.equal(r.status, 2);
|
||||
assert.match(r.stderr, /unknown agent: definitely-not-an-agent/);
|
||||
assert.match(r.stderr, /caveman --list/);
|
||||
});
|
||||
|
||||
test('--only known id passes argv validation', () => {
|
||||
// Dry-run + --only claude exits 0 even if the claude binary isn't on PATH.
|
||||
const r = run('--dry-run', '--only', 'claude', '--non-interactive', '--config-dir', '/tmp/__cm_only_test');
|
||||
assert.equal(r.status, 0);
|
||||
});
|
||||
|
||||
test('--config-dir expands ~ to home directory', async () => {
|
||||
// Pass `~/cm-test-…` and assert the dry-run plan resolves it relative to $HOME.
|
||||
// Use a unique suffix so the assertion is unambiguous.
|
||||
const suffix = `cm-test-${process.pid}`;
|
||||
const r = run('--dry-run', '--only', 'claude', '--non-interactive', '--config-dir', `~/${suffix}`);
|
||||
assert.equal(r.status, 0);
|
||||
// If the literal `~` had survived, we'd see `~/cm-test-…/hooks` in the plan.
|
||||
// The fix expands it in parseArgs, so we expect the absolute home path.
|
||||
assert.doesNotMatch(r.stdout, /~\/cm-test-/);
|
||||
// The plan only includes the hooks dir if claude is detected. Skip the
|
||||
// positive assertion when claude isn't on PATH on the runner.
|
||||
if (/Claude Code detected/.test(r.stdout)) {
|
||||
const { homedir } = await import('node:os');
|
||||
assert.match(r.stdout, new RegExp(homedir().replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '/' + suffix));
|
||||
}
|
||||
});
|
||||
|
||||
test('--help discloses --config-dir scope', () => {
|
||||
const r = run('--help');
|
||||
assert.equal(r.status, 0);
|
||||
// Disclosure: --config-dir does NOT scope third-party CLI invocations.
|
||||
// Help text wraps mid-phrase, so collapse whitespace before matching.
|
||||
const collapsed = r.stdout.replace(/\s+/g, ' ');
|
||||
assert.match(collapsed, /Does NOT scope/);
|
||||
assert.match(collapsed, /XDG_CONFIG_HOME/);
|
||||
assert.match(collapsed, /OPENCLAW_WORKSPACE/);
|
||||
});
|
||||
|
||||
@@ -113,6 +113,17 @@ test('hasCavemanHook detects via substring', () => {
|
||||
assert.equal(SETTINGS.hasCavemanHook(s, 'UserPromptSubmit'), false);
|
||||
});
|
||||
|
||||
test('removeCavemanHooks tolerates malformed hook event values without throwing', () => {
|
||||
// Pre-fix bug: settings.hooks.SessionStart = "oops" (string, not array)
|
||||
// would crash on .filter(...) inside the filter loop. Fix delegates to
|
||||
// validateHookFields first + adds Array.isArray guard.
|
||||
const s = { hooks: { SessionStart: "oops", UserPromptSubmit: { not: 'an array either' } } };
|
||||
let removed;
|
||||
assert.doesNotThrow(() => { removed = SETTINGS.removeCavemanHooks(s, 'caveman'); });
|
||||
assert.equal(removed, 0);
|
||||
assert.equal(s.hooks, undefined);
|
||||
});
|
||||
|
||||
test('removeCavemanHooks strips by marker and cleans empties', () => {
|
||||
const s = {
|
||||
hooks: {
|
||||
|
||||
Reference in New Issue
Block a user