diff --git a/README.md b/README.md index 6d0ff444..0b683253 100644 --- a/README.md +++ b/README.md @@ -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` | @@ -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. @@ -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.
Claude Code — full details @@ -248,7 +250,45 @@ Copilot works with Chat, Edits, and Coding Agent.
-Any other agent (opencode, Roo, Amp, Goose, Kiro, and 40+ more) +opencode — full details + +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 + +
+ +
+Any other agent (Roo, Amp, Goose, Kiro, and 40+ more) [npx skills](https://github.com/vercel-labs/skills) supports 40+ agents: @@ -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 | diff --git a/plugins/caveman/.opencode-plugin/package.json b/plugins/caveman/.opencode-plugin/package.json new file mode 100644 index 00000000..c5d2236d --- /dev/null +++ b/plugins/caveman/.opencode-plugin/package.json @@ -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" +} diff --git a/plugins/caveman/.opencode-plugin/server.ts b/plugins/caveman/.opencode-plugin/server.ts new file mode 100644 index 00000000..a6751e4b --- /dev/null +++ b/plugins/caveman/.opencode-plugin/server.ts @@ -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 } + +async function readState(): Promise { + try { + return JSON.parse(await fs.readFile(STATE_FILE, "utf8")) + } catch { + return { disabled: [], levels: {} } + } +} + +const RULES: Record = { + 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() + +async function server(_input: PluginInput): Promise { + 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 } diff --git a/plugins/caveman/.opencode-plugin/tui.ts b/plugins/caveman/.opencode-plugin/tui.ts new file mode 100644 index 00000000..7082636b --- /dev/null +++ b/plugins/caveman/.opencode-plugin/tui.ts @@ -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 } + +async function readState(): Promise { + 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({ + 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 }