Skip to content
Open
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
65 changes: 52 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ Pick your agent. One command. Done.
| **Cursor** | `npx skills add JuliusBrussee/caveman -a cursor` |
| **Windsurf** | `npx skills add JuliusBrussee/caveman -a windsurf` |
| **Copilot** | `npx skills add JuliusBrussee/caveman -a github-copilot` |
| **opencode** | Clone repo → add `.opencode-plugin/` path to `opencode.json` `plugin` array |
| **Cline** | `npx skills add JuliusBrussee/caveman -a cline` |
| **Any other** | `npx skills add JuliusBrussee/caveman` |

Expand All @@ -141,17 +142,17 @@ Install once. Use in every session for that install target after that. One rock.

Auto-activation is built in for Claude Code, Gemini CLI, and the repo-local Codex setup below. `npx skills add` installs the skill for other agents, but does **not** install repo rule/instruction files, so Caveman does not auto-start there unless you add the always-on snippet below.

| Feature | Claude Code | Codex | Gemini CLI | Cursor | Windsurf | Cline | Copilot |
|---------|:-----------:|:-----:|:----------:|:------:|:--------:|:-----:|:-------:|
| Caveman mode | Y | Y | Y | Y | Y | Y | Y |
| Auto-activate every session | Y | Y¹ | Y | —² | —² | —² | —² |
| `/caveman` command | Y | Y¹ | Y | — | — | — | — |
| Mode switching (lite/full/ultra) | Y | Y¹ | Y | Y³ | Y³ | — | — |
| Statusline badge | Y⁴ | — | — | — | — | — | — |
| caveman-commit | Y | — | Y | Y | Y | Y | Y |
| caveman-review | Y | — | Y | Y | Y | Y | Y |
| caveman-compress | Y | Y | Y | Y | Y | Y | Y |
| caveman-help | Y | — | Y | Y | Y | Y | Y |
| Feature | Claude Code | Codex | Gemini CLI | Cursor | Windsurf | Cline | Copilot | opencode |
|---------|:-----------:|:-----:|:----------:|:------:|:--------:|:-----:|:-------:|:--------:|
| Caveman mode | Y | Y | Y | Y | Y | Y | Y | Y |
| Auto-activate every session | Y | Y¹ | Y | —² | —² | —² | —² | Y⁵ |
| `/caveman` command | Y | Y¹ | Y | — | — | — | — | Y⁵ |
| Mode switching (lite/full/ultra) | Y | Y¹ | Y | Y³ | Y³ | — | — | Y⁵ |
| Statusline badge | Y⁴ | — | — | — | — | — | — | — |
| caveman-commit | Y | — | Y | Y | Y | Y | Y | Y |
| caveman-review | Y | — | Y | Y | Y | Y | Y | Y |
| caveman-compress | Y | Y | Y | Y | Y | Y | Y | — |
| caveman-help | Y | — | Y | Y | Y | Y | Y | — |

> [!NOTE]
> Auto-activation works differently per agent: Claude Code uses SessionStart hooks, this repo's Codex dogfood setup uses `.codex/hooks.json`, Gemini uses context files. Cursor/Windsurf/Cline/Copilot can be made always-on, but `npx skills add` installs only the skill, not the repo rule/instruction files.
Expand All @@ -160,6 +161,7 @@ Auto-activation is built in for Claude Code, Gemini CLI, and the repo-local Code
> ² Add the "Want it always on?" snippet below to those agents' system prompt or rule file if you want session-start activation.
> ³ Cursor and Windsurf receive the full SKILL.md with all intensity levels. Mode switching works on-demand via the skill; no slash command.
> ⁴ Available in Claude Code, but plugin install only nudges setup. Standalone `install.sh` / `install.ps1` configures it automatically when no custom `statusLine` exists.
> ⁵ Via the native opencode plugin (server + TUI). Enable/disable/level commands work per-session. Subagent sessions auto-use ultra.

<details>
<summary><strong>Claude Code — full details</strong></summary>
Expand Down Expand Up @@ -248,7 +250,45 @@ Copilot works with Chat, Edits, and Coding Agent.
</details>

<details>
<summary><strong>Any other agent (opencode, Roo, Amp, Goose, Kiro, and 40+ more)</strong></summary>
<summary><strong>opencode — full details</strong></summary>

Clone the repo, then add the plugin paths to your opencode config:

```bash
git clone https://github.com/JuliusBrussee/caveman
```

Add to `~/.config/opencode/opencode.json`:

```json
{
"plugin": [
"/path/to/caveman/plugins/caveman/.opencode-plugin/server.ts"
]
}
```

Add to `~/.config/opencode/tui.json`:

```json
{
"plugin": [
"/path/to/caveman/plugins/caveman/.opencode-plugin/tui.ts"
]
}
```

Auto-activates every session (full mode by default). Subagent sessions auto-use ultra.

TUI commands (via `/` command palette):
- `/enable_caveman` — enable for current session
- `/disable_caveman` — disable for current session
- `/caveman_level` — switch lite / full / ultra

</details>

<details>
<summary><strong>Any other agent (Roo, Amp, Goose, Kiro, and 40+ more)</strong></summary>

[npx skills](https://github.com/vercel-labs/skills) supports 40+ agents:

Expand Down Expand Up @@ -282,7 +322,6 @@ Code/commits/PRs: normal. Off: "stop caveman" / "normal mode".
Where to put it:
| Agent | File |
|-------|------|
| opencode | `.config/opencode/AGENTS.md` |
| Roo | `.roo/rules/caveman.md` |
| Amp | your workspace system prompt |
| Others | your agent's system prompt or rules file |
Expand Down
22 changes: 22 additions & 0 deletions plugins/caveman/.opencode-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "caveman-opencode",
"version": "0.1.0",
"description": "Caveman mode for opencode — cut filler, keep technical accuracy.",
"author": {
"name": "Julius Brussee",
"url": "https://github.com/JuliusBrussee"
},
"homepage": "https://github.com/JuliusBrussee/caveman",
"repository": "https://github.com/JuliusBrussee/caveman",
"license": "MIT",
"keywords": [
"productivity",
"caveman",
"brevity"
],
"exports": {
"./server": "./server.ts",
"./tui": "./tui.ts"
},
"main": "./server.ts"
}
68 changes: 68 additions & 0 deletions plugins/caveman/.opencode-plugin/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import path from "path"
import fs from "fs/promises"
import os from "os"

const STATE_FILE = path.join(
process.env.XDG_STATE_HOME ?? path.join(os.homedir(), ".local", "state"),
"opencode",
"caveman.json",
)

type Level = "lite" | "full" | "ultra"
type State = { disabled: string[]; levels: Record<string, Level> }

async function readState(): Promise<State> {
try {
return JSON.parse(await fs.readFile(STATE_FILE, "utf8"))
} catch {
return { disabled: [], levels: {} }
}
}

const RULES: Record<Level, string> = {
lite: `[CAVEMAN MODE: lite] Respond terse. No filler/hedging. Keep articles + full sentences. Professional but tight.
Drop: filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging.
Technical terms exact. Code blocks unchanged. Errors quoted exact.
Auto-clarity: full language for security warnings + irreversible ops.`,

full: `[CAVEMAN MODE: full] Respond terse like smart caveman. All technical substance stay. Only fluff die.
Drop: articles (a/an/the), filler, pleasantries, hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for").
Pattern: \`[thing] [action] [reason]. [next step].\`
Technical terms exact. Code blocks unchanged. Errors quoted exact.
Auto-clarity: full language for security warnings + irreversible ops.`,

ultra: `[CAVEMAN MODE: ultra] Respond ultra-terse. Max compression. All technical substance preserved.
Abbreviate (DB/auth/config/req/res/fn/impl). Strip articles+conjunctions+filler+hedging+pleasantries. Arrows for causality (X → Y). One word when sufficient. Fragments OK.
Technical terms exact. Code blocks unchanged. Errors quoted exact.
Auto-clarity: full language for security warnings + irreversible ops only.`,
}

// Track subagent sessions — auto-apply ultra for them
const childSessions = new Set<string>()

async function server(_input: PluginInput): Promise<Hooks> {
return {
event: async ({ event }: any) => {
if (event?.type === "session.created") {
const info = event?.properties?.info
if (info?.parentID) childSessions.add(info.id)
}
if (event?.type === "session.deleted") {
const sid = event?.properties?.sessionID
if (sid) childSessions.delete(sid)
}
},

"experimental.chat.system.transform": async (input, output) => {
const sid = input.sessionID
if (!sid) return
const state = await readState()
if (state.disabled.includes(sid)) return
const level: Level = childSessions.has(sid) ? "ultra" : (state.levels[sid] ?? "full")
output.system.push(RULES[level])
},
}
}

export default { id: "caveman", server }
93 changes: 93 additions & 0 deletions plugins/caveman/.opencode-plugin/tui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { TuiPlugin } from "@opencode-ai/plugin/tui"
import path from "path"
import fs from "fs/promises"
import os from "os"

const STATE_FILE = path.join(
process.env.XDG_STATE_HOME ?? path.join(os.homedir(), ".local", "state"),
"opencode",
"caveman.json",
)

type Level = "lite" | "full" | "ultra"
type State = { disabled: string[]; levels: Record<string, Level> }

async function readState(): Promise<State> {
try {
return JSON.parse(await fs.readFile(STATE_FILE, "utf8"))
} catch {
return { disabled: [], levels: {} }
}
}

async function writeState(state: State) {
await fs.mkdir(path.dirname(STATE_FILE), { recursive: true })
await fs.writeFile(STATE_FILE, JSON.stringify(state), "utf8")
}

const tui: TuiPlugin = async (api) => {
function sid(): string | undefined {
const r = api.route.current
return r.name === "session" ? (r as any).params?.sessionID : undefined
}

api.command.register(() => [
{
title: "Enable caveman mode",
value: "caveman.enable",
category: "Caveman",
slash: { name: "enable_caveman" },
onSelect: async () => {
const id = sid()
if (!id) return
const state = await readState()
state.disabled = state.disabled.filter((x) => x !== id)
await writeState(state)
api.ui.toast({ variant: "success", message: "Caveman enabled (full mode)" })
},
},
{
title: "Disable caveman mode",
value: "caveman.disable",
category: "Caveman",
slash: { name: "disable_caveman" },
onSelect: async () => {
const id = sid()
if (!id) return
const state = await readState()
if (!state.disabled.includes(id)) state.disabled.push(id)
await writeState(state)
api.ui.toast({ variant: "info", message: "Caveman disabled" })
},
},
{
title: "Set caveman level",
value: "caveman.level",
category: "Caveman",
slash: { name: "caveman_level" },
onSelect: async () => {
const id = sid()
if (!id) return
api.ui.dialog.replace(() =>
api.ui.DialogSelect<Level>({
title: "Caveman level",
options: [
{ title: "full", value: "full", description: "Classic caveman. Drop articles, fragments OK. (default)" },
{ title: "lite", value: "lite", description: "No filler. Full sentences. Professional." },
{ title: "ultra", value: "ultra", description: "Max compression. Abbreviations, arrows." },
],
onSelect: async (opt) => {
api.ui.dialog.clear()
const state = await readState()
state.levels[id] = opt.value
await writeState(state)
api.ui.toast({ variant: "success", message: `Caveman level: ${opt.value}` })
},
}),
)
},
},
])
}

export default { id: "caveman-tui", tui }