[Sync] llm stuff

This commit is contained in:
Coja
2026-09-17 20:58:21 +02:00
parent ed7e34552d
commit d8f6b30e2c
52 changed files with 2233 additions and 237 deletions
+6 -3
View File
@@ -168,9 +168,12 @@ track neither. HyDE-managed customization goes via:
- `gui/.config/waybar/modules/*.jsonc` — module defs, globbed into the generated config.
**The custom cava rig is NOT HyDE-managed**: `layouts/custom.jsonc` + `layouts/custom.css` form a
self-contained two-bar config launched raw (`waybar -c …custom.jsonc -s …custom.css` — exec-once,
the Ctrl+Alt+W / Super+Shift+R binds, and the `wbar` abbr). **Never route it through
`hyde-shell waybar --set`** — waybar.py would clobber the repo's files as above. HyDE only reads
self-contained two-bar config run by `wbar` (`gui/.local/bin/wbar`) as the supervised systemd
user unit `dots-waybar.service` (`Restart=always` — waybar 0.15.0 dies on every DP-2 link
drop/return, and as a bare process nothing relaunched it). The `exec-once` in `userprefs.conf`,
the Ctrl+Alt+W / Super+Shift+R binds and the `wbar` command all go through it; **never `killall
waybar`** — systemd brings it back in seconds and a raw relaunch beside it means two bars
(`wbar toggle|restart|reload|stop`). **Never route it through `hyde-shell waybar --set`** — waybar.py would clobber the repo's files as above. HyDE only reads
from `layouts/`, which is what makes them safe to track. On a rig host, HyDE's own bar must be
disabled once, or **two bars appear at every login** — and the launcher differs by HyDE vintage:
+13 -11
View File
@@ -1,3 +1,4 @@
# NOTE: the lw/ and wm/ host overlays carry full-file copies of this config — mirror changes there.
# see https://github.com/sigoden/aichat/blob/main/config.example.yaml
keybindings: vi
editor: nvim
@@ -22,34 +23,35 @@ clients:
name: local
api_base: http://192.168.0.204:11343/v1
models: &lan_models
# Speeds are benched decode t/s (clean-night sweeps) — see fl/.config/llamacpp/README.md roster.
- name: Qwen3-Coder-30B-Instruct-UD-Q3_K_XL
max_input_tokens: 30000 # ctx 32768
max_input_tokens: 30000 # ctx 32768 · ~30 t/s — main agent coder
- name: Qwen3-Coder-Next-UD-IQ3_XXS
max_input_tokens: 128000 # ctx 131072
max_input_tokens: 128000 # ctx 131072 · ~16 t/s — long sessions (128k)
- name: Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS
max_input_tokens: 22000 # ctx 24576
max_input_tokens: 22000 # ctx 24576 · ~36 t/s — daily driver
supports_vision: true
- name: Qwen3.6-35B-A3B-Thinking
max_input_tokens: 22000 # ctx 24576
max_input_tokens: 22000 # ctx 24576 · ~39 t/s — hard problems
supports_vision: true
- name: Qwen3.5-9B-UD-Q6_K_XL
max_input_tokens: 30000 # ctx 32768
max_input_tokens: 30000 # ctx 32768 · ~32 t/s — quick tasks
supports_vision: true
- name: Qwen3.8-27B-UD-IQ3_XXS
max_input_tokens: 22000 # ctx 24576
max_input_tokens: 22000 # ctx 24576 · ~15 t/s — hybrid reasoner, quiet desktop only
supports_vision: true
- name: gemma-4-26B-A4B-it-UD-IQ4_XS
max_input_tokens: 22000 # ctx 24576
max_input_tokens: 22000 # ctx 24576 · ~34 t/s — quality generalist, best vision
supports_vision: true
- name: gemma-4-E4B-it-UD-Q8_K_XL
max_input_tokens: 62000 # ctx 65536
max_input_tokens: 62000 # ctx 65536 · ~57 t/s — fast generalist, long docs
supports_vision: true
- name: GLM-4.7-Flash-UD-Q4_K_XL
max_input_tokens: 22000 # ctx 24576
max_input_tokens: 22000 # ctx 24576 · ~21 t/s — quality coder
- name: gpt-oss-20b
max_input_tokens: 62000 # ctx 65536
max_input_tokens: 62000 # ctx 65536 · ~38 t/s — fast reasoning + tools
- name: gpt-oss-20b-low
max_input_tokens: 62000 # ctx 65536
max_input_tokens: 62000 # ctx 65536 · ~37 t/s — snappy answers
# Remote access via duskadiy.com (reachable from anywhere; requires an API key —
# $DUSKADIY_API_KEY, from the gitignored conf.d/secrets.fish). The LAN endpoint above is keyless.
# Same server, same model ids — reuse the list above via a YAML anchor.
+10 -4
View File
@@ -8,14 +8,20 @@
- The user self-hosts models on the LAN at `192.168.0.204`:
- llama.cpp server — `http://192.168.0.204:11343/v1` (OpenAI-compatible).
- Ollama — `192.168.0.204:11434`.
- Recurring models: Qwen3-Coder-30B, gemma-4-26B, DeepSeek-Coder-V2-Lite, GLM-4.7-Flash. When configuring any AI/agent tool, default to these endpoints rather than a cloud API unless asked otherwise.
- The available models are the section names in `~/.dots/fl/.config/llamacpp/config.ini` (the router's client model ids) — read that file rather than trusting any list written here; the roster changes often. When configuring any AI/agent tool, default to these endpoints rather than a cloud API unless asked otherwise.
## Git
- **Do not run `git commit` or `git push`, and never offer or ask to do the committing/pushing yourself** — the user reviews and commits entirely manually (these are also denied in settings.json). Stage/prepare changes and stop there. You may suggest a commit message for the user to use, but nothing more.
- To hand off a suggested commit message, write it to the per-worktree file `$(git rev-parse --git-path CLAUDE_COMMIT_MSG)`**not** `COMMIT_EDITMSG` (git overwrites that on `git commit`). A `prepare-commit-msg` hook prefills the editor from it (one-shot) on the next `git commit` / lazygit `C`. Just the raw message, no `#` comments.
- **Do not run `git commit` or `git push`, and never offer or ask to do the committing/pushing yourself** — the user reviews and commits entirely manually (denied in settings.json and enforced by the `~/.claude/hooks/git-guard.py` PreToolUse hook, which catches `git -C … push`-style spellings the prefix rules miss and also blocks history rewrites and working-tree discards — `revert`, `cherry-pick`, `rebase`, `merge`, `reset` that moves HEAD or unstages everything, `restore <paths>`, `checkout --`, `stash drop/pop/clear`, `branch -D` — plus `gh pr merge/create/close/edit`; several sessions share this checkout, so never discard changes you did not make). Stage/prepare changes and stop there. You may suggest a commit message for the user to use, but nothing more.
- To hand off a suggested commit message, feed it to `~/.config/git/claude-commit-msg save` **after staging** (it records the staged paths with the message), as a quoted heredoc so quotes and backticks in the body survive:
```sh
~/.config/git/claude-commit-msg save <<'EOF'
<message>
EOF
```
Never write `CLAUDE_COMMIT_MSG` or `COMMIT_EDITMSG` directly — several sessions share this checkout, and the single file got overwritten. A `prepare-commit-msg` hook prefills the next `git commit` / lazygit `C` with the message(s) whose recorded paths are part of that commit, then consumes them. Just the raw message, no `#` comments. `claude-commit-msg list` shows what is pending.
## Formatting
- Lua: format with `stylua` (2-space indent, 120 col, no call parentheses — see any `.stylua.toml`).
## Dotfiles
- Configs live in the dots repo under `.config/` and are surfaced via per-directory symlinks into `~/.config/` (and `~/.claude/`). Editing a file under the repo's `.config/` is editing the live config — no deploy step.
- Configs live in the dots repo under `.config/` and are surfaced via **per-file** symlinks (GNU Stow `--no-folding`; directories stay real) into `~/.config/`, and from there into `~/.claude/` by `~/.config/claude/link.sh`. Editing an existing file under the repo's `.config/` is editing the live config — no deploy step. A **new** file is not live until a re-stow creates its symlink (`cd ~/.dots && ./install.sh`), on each host.
+67 -9
View File
@@ -7,20 +7,65 @@ back into this repo.
## What's tracked here
- `settings.json` — global settings: permissions (deny git commit/push + read of secret
files), default `model`, `enabledPlugins`, vim mode, dark theme, `effortLevel`, fullscreen
TUI, `statusLine`, `hooks`, `worktree` defaults, …
- `settings.json` — global settings: permissions (deny git commit/push, `gh pr` write ops, and
reads of secret files), default `model`, `enabledPlugins`, vim mode, dark theme, `effortLevel`,
fullscreen TUI, `statusLine`, `hooks`, `worktree` defaults, a generic `autoMode.environment`,
Nothing in here may name a specific project, path or remote: this file is published by the
public mirror, and the auto-mode classifier only reads `autoMode` from user/managed settings
anyway (never from a project's `.claude/settings*.json`).
- `statusline.py` — rich status line (dir, git, model, context %, cost, disk — system metrics live in the tmux bar).
See [StatusBar.md](StatusBar.md) for what each segment and colour means.
- `keybindings.json` — custom keybindings (vim-style scroll/navigation).
- `keybindings.json` — custom keybindings: vim-style scroll/navigation, plus `ctrl+x v` for the
verbose transcript view (`app:toggleTranscript`, same keystroke as opencode's `<leader>v`; the
default `ctrl+o` still works, and `ctrl+e` inside the view expands everything).
- `CLAUDE.md` — global user preferences applied to every project.
- `hooks/format.py` — PostToolUse hook: formats edited files by extension (stylua / ruff /
prettier / gofmt / rustfmt / fish_indent / shfmt / taplo). Opinionated formatters (stylua, ruff,
prettier, taplo) only run where the project opts in via a config file found walking up; gofmt and
rustfmt gate on a project marker (`go.mod` / `Cargo.toml`); fish_indent and shfmt run on sight.
No-ops if the formatter isn't installed, and always exits 0.
prettier / gofmt / rustfmt / fish_indent / shfmt / taplo). Every formatter is opt-in: stylua,
ruff, prettier and taplo run only where the project has their config file (found walking up);
gofmt and rustfmt gate on a project marker (`go.mod` / `Cargo.toml`); fish_indent and shfmt gate
on an `.editorconfig` *section* that matches the file. Wired to `Edit|Write` **and** `Bash`
under auto mode Claude edits through the shell, so the Bash branch scans the command for
path-like tokens and formats the ones modified in the last 30 s. No-ops if the formatter isn't
installed, and always exits 0.
- `hooks/git-guard.py` — PreToolUse hook on `Bash`: structurally blocks anything that commits,
rewrites history or discards working-tree changes — `git commit` / `push` / `revert` /
`cherry-pick` / `rebase` / `merge` (not `--abort`), `clean` / `reset` in any form that moves HEAD
or unstages everything (`reset -- <paths>`, `reset HEAD <path>` stay allowed) / `restore <paths>`
(`--staged <path>` is fine, `--staged .` is not) / `checkout --` / `checkout .` / `stash drop|pop|clear` /
`branch -D|-f|-M`, and `gh pr merge` / `create` / `close` / `edit` — wherever the flags sit
(`git -C . push`, `/usr/bin/git push`, `sh -c 'git push'`), which the prefix-matched deny rules
can't. The discard group is what protects parallel sessions sharing one checkout. Exit 2 + a
one-line reason. Heredoc bodies are skipped on purpose (file contents). Regression matrix:
`bash ~/.claude/hooks/git-guard-test.sh` (a file on purpose — typed inline, the commands under
test would trip the live hook).
- `hooks/notify.py` — Notification hook (`idle_prompt|permission_prompt`): `notify-send` desktop
notification when Claude is waiting on you; no-op on a headless host.
- `hooks/drift-check.sh` — SessionStart + UserPromptSubmit hook, read-only: fails loudly when a
`~/.config/claude/*` file has broken out of the repo into a plain file, or `~/.claude/*` no longer
resolves into the dots repo. That is what Claude Code's atomic settings rewrite does through the
stow layer — on 2026-09-15 it silently reverted the live config to a stale snapshot for an hour,
dropping four hook events. At session start the warning is shown to you (exit 2, session goes
on); on each prompt any drift is fed to Claude as context so it reports it. Fix is still
`link.sh` (heals identical copies, warns otherwise).
- `hooks/local-commands.py` — UserPromptExpansion hook: **TUI-side slash commands**. When you type
`/x` and `skills/x/local.py` exists, the hook runs it with the arguments, shows its output to you
and blocks the expansion (exit 2), so no prompt reaches the model and it costs nothing. Skills
without a `local.py` expand as usual. Caveat baked into the design: the hook fires *after* the
skill body expanded, so a skill with a `local.py` must not also act via a `!` inline command.
- `skills/` — custom [Agent Skills](https://code.claude.com/docs/en/skills). See **Skills** below.
- `link.sh` — idempotent bootstrap that creates the `~/.claude` symlinks below.
- `../git/claude-commit-msg` + `../git/hooks/prepare-commit-msg` — the commit-message handoff.
A session pipes its suggestion into `claude-commit-msg save`, which stores it as its own file under
`$(git rev-parse --git-path CLAUDE_COMMIT_MSG.d)/` **with the paths it staged**; on the next
`git commit` / lazygit `C` the hook prefills the editor with the message(s) whose paths are part of
that commit and consumes them (`used/`). Parallel sessions therefore never overwrite each other,
and committing one session's files gets that session's message. `claude-commit-msg list` shows
what is pending. The old single `CLAUDE_COMMIT_MSG` file is still honoured. Both tracked here
(deployed to `~/.config/git/`); the hook is installed **per repo** — see setup step 4 — and
deliberately not via a global `core.hooksPath`, which would override a project's own hooks.
The `settings.json` hook commands for the newer scripts are wrapped in `[ -f … ]`, so a host that
has pulled the repo but not yet re-stowed just skips them instead of blocking every Bash call.
Paths inside `settings.json` reference `~/.claude/...` via `$HOME`/`PATH` (`/usr/bin/env
python3 ~/.claude/...`), so they work regardless of the username, python location, or where the
@@ -43,7 +88,13 @@ claude
# 3. Reinstall the plugins from settings.json -> enabledPlugins
# (typescript-lsp, frontend-design — anthropics/claude-plugins-official) via /plugin.
# Optional: install any formatters you want the hook to use (stylua, ruff, prettier, …).
# Optional: install any formatters you want the hook to use (stylua, ruff, prettier, …),
# and libnotify (notify-send) on GUI hosts for hooks/notify.py.
```
```fish
# 4. In every repo you commit in: install the one-shot commit-message prefill hook
ln -sf ~/.config/git/hooks/prepare-commit-msg (git rev-parse --git-path hooks)/prepare-commit-msg
```
`install.sh` deploys `~/.config/claude` (via stow) and then runs `link.sh` automatically. You only
@@ -73,6 +124,13 @@ so Claude never auto-triggers them. Run them inside the repo whose branch you're
- **`/commit-msg [hint]`** — reads the **staged** diff, infers the repo's commit style from recent
`git log` (e.g. this repo's `[Scope] summary`), drafts a matching message, prints it, and copies
it to the clipboard with `wl-copy`. Never stages or commits — draft only.
- **`/verbose [on|off|status]`** — flips the *persisted* verbose view for the current project:
`viewMode` (`verbose` / `default` — the key that actually decides the view and beats a sticky
`/focus` choice for new sessions) plus `verbose` and `showThinkingSummaries`, written to the
project's `.claude/settings.local.json` (machine-local, never committed) rather than the tracked
`settings.json`, so a toggle never shows up as a dots diff. Runs **TUI-side**`local.py` via
`hooks/local-commands.py`, no model turn — with the SKILL.md body as the fallback on a host
where the hook isn't deployed. The *live* toggle is a keypress: `ctrl+x v` / `ctrl+o`.
- **`/pr-loop [pr]`** — babysits the current branch's PR in a self-paced loop: fixes review
comments that don't need your input — from **any** reviewer (Codex, Claude, humans) plus any
`@claude` request — `git add`s them, drafts the commit message (`/commit-msg` style) and marks
+4 -3
View File
@@ -8,7 +8,7 @@ resize), trailing (lowest-priority) segments are dropped from the right until it
Wide — everything:
```
📁 ~/.dots │ 🌿 .dots main* │ 🤖 Fable 5 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m │ 💰 $0.40 $12.00/h │ 💾 81%
📁 ~/.dots │ 🌿 .dots main* │ 🤖 Fable 5 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m │ 💰 $0.40 $12.00/h │ ♻️ 91% 1h │ 💾 81%
```
Narrow — trailing segments trimmed:
@@ -29,9 +29,10 @@ is supplementary to the main colored value.
| 📁 | **Directory** | Current working dir, with `~` for home | cyan |
| 🌿 | **Git** | `repo branch` + `*` if dirty + `↑N`/`↓N` ahead/behind upstream | repo = magenta; branch = green when clean, yellow + red `*` when dirty; `↑` cyan, `↓` yellow. Omitted outside a repo |
| 🤖 | **Model** | Active model display name + `· effort` level (`low`/`medium`/`high`/`xhigh`) | name = blue, effort = light gray; effort omitted for models without the param |
| 📝 | **Context** | `% of context window used` + `(Nk)` tokens | window comes from the payload's `context_window.context_window_size`, falling back to `(1M …)` in the display name, else 200k — **not** from `model.id`, which arrives with any `[1m]` suffix already stripped. Adds a red **⚠compact** at ≥80% |
| 📝 | **Context** | `% of context window used` + `(Nk)` tokens | usage comes from the payload's `context_window` (`used_percentage` + `current_usage`, Claude Code 2.1.x); the transcript's last `message.usage` is only a fallback. Window from `context_window.context_window_size`, then `(1M …)` in the display name, else 200k — **not** from `model.id`, which arrives with any `[1m]` suffix already stripped. Adds a red **⚠compact** at ≥80% |
| 📊 | **5h usage** | `% of the 5-hour rolling limit used` + time until it resets | from `rate_limits.five_hour`; Pro/Max only, and absent until the first API response of a session |
| 💰 | **Cost** | `$` session cost so far + `$/h` burn rate | burn rate shown once the session exceeds ~30s |
| ♻️ | **Prompt cache** | hit `%` (higher is better) + cache TTL (`5m` / `1h`), yellow `·cold` once it has expired | from the payload's `prompt_cache` (2.1.260+: `hit_ratio` 01, `ttl`, `warm`); computed from usage on older payloads. A `1h` TTL is what keeps a 10-min `/pr-loop` cadence warm |
| 💾 | **Disk** | `% used` of the filesystem at the cwd | from `statvfs` |
## Colour legend
@@ -43,6 +44,7 @@ attention. Light gray = secondary detail. Thresholds (`green < … < yellow <
|---|---|---|---|
| Context | `< 50%` | `5080%` | `≥ 80%` (⚠compact) |
| 5h usage | `< 50%` | `5080%` | `≥ 80%` |
| Prompt cache (inverted) | `≥ 80%` | `5080%` | `< 50%` |
| Disk | `< 75%` | `7590%` | `≥ 90%` |
> Light gray is `\033[37m`; dimming (`\033[2m`) is disabled because it blended the gray
@@ -64,7 +66,6 @@ events, so system metrics sat visibly stale between turns — the tmux status ba
| 🖥️ | `cpu_segment` | `% busy` (diffed `/proc/stat` snapshot) + loadavg + cores (green `<60%` / red `≥85%`) |
| 🌡️ | `temp_segment` | hottest CPU thermal zone °C (green `<60` / red `≥80`) |
| ✏️ | `velocity_segment` | `+added/-removed` lines this session + lines/min |
| ♻️ | `cache_segment` | prompt-cache hit % (higher is better) |
| ⚙️ | `api_segment` | share of wall-clock time spent in API/inference |
| | `version_segment` | Claude Code version (`vX.Y.Z`) |
| 🎨 | `style_segment` | active output-style name |
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# SessionStart hook: refuse to let a severed dotfiles link go unnoticed.
#
# Claude Code writes settings with an atomic temp+rename. Through the stow layer that can
# replace ~/.config/claude/<file> (a symlink into the repo) with a plain FILE holding whatever
# snapshot the writing process had in memory -- silently freezing every later repo edit out
# of the live config. It happened on 2026-09-15 23:31: a stale copy dropped four hook events
# (git-guard among them) and nobody noticed for an hour. link.sh heals the identical case but
# only when run; this makes every session start say so instead.
#
# Read-only: it never relinks. Exit 2 on SessionStart shows stderr to the user and the session
# continues. Exit 0 when everything resolves into the repo.
set -u
src="$HOME/.config/claude"
bad=""
for f in settings.json statusline.py keybindings.json CLAUDE.md link.sh; do
p="$src/$f"
[ -e "$p" ] || continue
if [ ! -L "$p" ]; then bad="$bad $p is a plain file (should be a symlink into the dots repo)"$'\n'; fi
done
# the ~/.claude side too: link.sh's own guard covers it, but only when link.sh runs
for f in settings.json keybindings.json CLAUDE.md statusline.py; do
p="$HOME/.claude/$f"
[ -e "$p" ] || continue
case "$(readlink -f "$p")" in
*"/.dots/"*) ;;
*) bad="$bad $p resolves to $(readlink -f "$p"), not into the dots repo"$'\n' ;;
esac
done
[ -z "$bad" ] && exit 0
printf 'DRIFT: Claude config has broken out of the dots repo; the live config is a stale copy.\n%s fix: review with `diff`, then `bash ~/.config/claude/link.sh` (heals identical copies, warns otherwise)\n' "$bad" >&2
exit 2
+55 -9
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""PostToolUse hook: format an edited file with the right formatter.
Reads the hook JSON from stdin, extracts the edited file path, and runs a
Reads the hook JSON from stdin, works out which file(s) were just edited, and runs a
language-appropriate formatter -- but, to avoid imposing a style on a project
that never asked for it, every formatter here only runs when the project opts in
via a config file discoverable by walking up from the edited file: stylua, ruff,
@@ -9,6 +9,15 @@ prettier and taplo on their own configs, gofmt and rustfmt on go.mod / Cargo.tom
shfmt and fish_indent on .editorconfig. No marker, no formatting -- a repo that
hand-formats its shell keeps its own layout.
Two entry points, both wired in settings.json > hooks > PostToolUse:
* Edit|Write -- the edited path is `tool_input.file_path`.
* Bash -- under `defaultMode: auto` Claude is steered to edit through the shell
(sed -i, heredocs, python), which the Edit|Write matcher never sees.
There is no per-file event for that (`FileChanged` only watches literal
filenames), so this scans `tool_input.command` for path-like tokens with a
handled extension and formats the ones modified in the last few seconds --
the mtime gate keeps a parallel session's dirty files out of it.
Best-effort: always exits 0 so a format hiccup never blocks an edit. jq is not
guaranteed on the host, hence python for the stdin JSON parse.
"""
@@ -19,6 +28,7 @@ import re
import shutil
import subprocess
import sys
import time
def find_up(start, names):
@@ -178,14 +188,13 @@ HANDLERS = {
}
def main():
try:
data = json.load(sys.stdin)
except Exception:
return
ti = data.get("tool_input") or {}
tr = data.get("tool_response") or {}
path = ti.get("file_path") or tr.get("filePath") or ""
RECENT_SECS = 30 # a Bash edit is "ours" if the file changed this recently
_EXT_ALT = "|".join(re.escape(e[1:]) for e in HANDLERS)
_PATHISH = re.compile(r"[\w.~/@+-]+\.(?:" + _EXT_ALT + r")\b")
def format_one(path):
if not path or not os.path.isfile(path):
return
# Resolve symlinks before walking for markers: deployed dotfiles get edited via
@@ -204,5 +213,42 @@ def main():
pass
def recent_paths_in_command(command, cwd):
"""Files a shell command plausibly just wrote: path-like tokens with a handled
extension, resolved against the tool's cwd, that exist and changed within RECENT_SECS.
Unchanged mentions (the source of a `cp`, a file merely grepped) fail the mtime gate."""
now = time.time()
seen, out = set(), []
for tok in _PATHISH.findall(command or ""):
p = os.path.expanduser(tok)
if not os.path.isabs(p):
p = os.path.join(cwd, p)
p = os.path.normpath(p)
if p in seen:
continue
seen.add(p)
try:
if os.path.isfile(p) and now - os.path.getmtime(p) <= RECENT_SECS:
out.append(p)
except OSError:
pass
return out
def main():
try:
data = json.load(sys.stdin)
except Exception:
return
ti = data.get("tool_input") or {}
tr = data.get("tool_response") or {}
if data.get("tool_name") == "Bash":
cwd = data.get("cwd") or os.getcwd()
for p in recent_paths_in_command(ti.get("command"), cwd):
format_one(p)
return
format_one(ti.get("file_path") or tr.get("filePath") or "")
if __name__ == "__main__":
main()
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# Regression matrix for git-guard.py: `bash ~/.claude/hooks/git-guard-test.sh` (exit 1 on mismatch).
# Kept as a file on purpose: typed inline, the very commands under test would trip the live hook.
G="$(dirname "$(readlink -f "$0")")/git-guard.py"; fail=0
# `git reset <one arg>` is allowed only for an existing path: give the matrix one to point at.
tmp=$(mktemp -d) && cd "$tmp" && touch existing.py
trap 'rm -rf "$tmp"' EXIT
run() { printf '%s' "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$2")}}" | python3 "$G" 2>/dev/null; rc=$?
got=allow; [ $rc -eq 2 ] && got=block; [ "$got" = "$1" ] && mark=ok || { mark=MISMATCH; fail=1; }; printf '%-8s %-6s %-6s %s\n' "$mark" "$1" "$got" "$(printf '%s' "$2" | head -1)"; }
echo "--- must block: commits / history ---"
run block 'git push'
run block 'git -C /home/coja/.dots push origin main'
run block 'git -c user.name=x commit -m fix'
run block '/usr/bin/git push'
run block 'env GIT_DIR=.git git push'
run block 'timeout 5 git push'
run block 'sudo -u me git push'
run block 'cd /tmp && git push'
run block 'git status; git commit -am wip'
run block 'x=1; git push'
run block 'git push 2>&1 | tee log'
run block "sh -c 'git push origin HEAD'"
run block 'eval "git push"'
run block 'git -C $(pwd) push'
run block 'echo "$(git push)"'
run block 'git --git-dir=/x/.git --work-tree=/x push'
run block 'git commit -m "fix: grep pattern; not a command"'
run block 'git revert HEAD'
run block 'git cherry-pick abc123'
run block 'git rebase -i HEAD~3'
run block 'git rebase main'
run block 'git merge feature'
run block 'git merge --no-ff feature'
echo "--- must block: discards working-tree changes ---"
run block 'git reset --hard HEAD~1'
run block 'git reset HEAD~1 --hard'
run block 'git reset --soft HEAD~1'
run block 'git reset --keep HEAD~2'
run block 'git reset --merge'
run block 'git reset'
run block 'git reset -q'
run block 'git reset HEAD'
run block 'git reset HEAD~1'
run block 'git reset abc1234'
run block 'git reset 0123456789abcdef0123456789abcdef01234567'
run block 'git reset origin/main'
run block 'git reset @{u}'
run block 'git restore --staged .'
run block 'git restore --staged :/'
run block 'git clean -fd'
run block 'git -C . clean -n'
run block 'git restore file.py'
run block 'git restore --staged --worktree file.py'
run block 'git restore -W file.py'
run block 'git checkout -- .'
run block 'git checkout -- file.py'
run block 'git checkout HEAD -- file.py'
run block 'git checkout .'
run block 'git checkout -f main'
run block 'git stash drop'
run block 'git stash pop'
run block 'git stash clear'
run block 'git branch -D feature'
run block 'git branch -f main HEAD~1'
run block 'git branch -M main trunk'
echo "--- must block: gh pr write ops ---"
run block 'gh pr merge 12 --squash'
run block 'gh -R o/r pr create --fill'
run block 'gh pr edit 3 --title x'
run block 'gh pr close 4'
echo "--- must allow ---"
run allow 'git status'
run allow 'git log -1 --oneline'
run allow 'git diff --cached --stat'
run allow 'git add -A common/.config/claude'
run allow 'git rev-parse --git-path CLAUDE_COMMIT_MSG'
run allow 'git -c core.pager=cat log -3'
run allow 'git reset -- file.txt'
run allow 'git reset HEAD file.txt'
run allow 'git reset HEAD existing.py other.py'
run allow 'git reset existing.py'
run allow 'git reset -p'
run allow 'git reset -- .'
run allow 'git restore --staged existing.py'
run allow 'git restore --staged file.py'
run allow 'git restore -S file.py'
run allow 'git checkout main'
run allow 'git checkout -b feature'
run allow 'git switch main'
run allow 'git merge --abort'
run allow 'git stash'
run allow 'git stash list'
run allow 'git stash push -m wip'
run allow 'git stash show -p'
run allow 'git branch -d merged-branch'
run allow 'git branch --list'
run allow 'git branch -vv'
run allow 'git commit-tree HEAD^{tree} -m x'
run allow 'git worktree list'
run allow 'grep -rn "git push" README.md'
run allow 'grep -n "foo; git commit" file'
run allow "echo 'git push'"
run allow 'echo "run git push"'
run allow "cat > README.md <<'EOF'
Deploy with:
git status; git commit -am x; git push origin main
EOF
echo written"
run allow 'python3 - <<EOF
print("git commit is manual")
EOF'
run allow 'gh pr view 12 --json updatedAt'
run allow 'gh api graphql -f query="mutation{...}"'
run allow 'gh auth status'
run allow 'ls -la HEAD#x; git status'
run allow 'git log --grep="git push" -5'
run allow '~/.config/git/claude-commit-msg list'
echo "--- fail-open ---"
printf '%s' '{"tool_name":"Edit","tool_input":{"command":"git push"}}' | python3 "$G"; [ $? -eq 0 ] && echo "ok non-Bash tool ignored" || { echo "MISMATCH non-Bash"; fail=1; }
printf 'not json' | python3 "$G"; [ $? -eq 0 ] && echo "ok garbage stdin passes" || { echo "MISMATCH garbage"; fail=1; }
[ $fail -eq 0 ] && echo "GIT-GUARD MATRIX: all ok" || { echo "GIT-GUARD MATRIX: mismatches above"; exit 1; }
+208
View File
@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""PreToolUse hook (Bash): enforce the no-commit / no-push contract structurally.
settings.json's deny rules (`Bash(git push:*)`, ...) match a literal *prefix* of the
command text, so `git -C . push`, `git -c k=v commit`, `/usr/bin/git push`,
`env X=1 git push`, `timeout 5 git push` and `sh -c 'git push'` all slip past them.
This hook parses each shell segment instead: finds the git/gh token wherever it sits,
skips git's global options (`-C dir`, `-c k=v`, `--git-dir=...`) and blocks
git commit | push | revert | cherry-pick | rebase | merge (not --abort/--quit/--continue)
git clean | reset (any form that moves HEAD or unstages everything; `reset -- <paths>`,
`reset HEAD <path>` and `reset <existing path>` are fine) | restore <paths> (--staged
alone is fine, except `--staged .` / `:/`) | checkout -- / checkout . / -f
git stash drop | pop | clear git branch -D / -f / -M
gh pr merge | create | close | edit
i.e. anything that commits, rewrites history, or discards working-tree changes -- the last
group matters most with several sessions sharing one checkout. Mirrors pi's permission-gate.
Exit 2 + one stderr line = the call is blocked and Claude reads the reason (stage and
stop; /commit-msg drafts the message). Mirrors pi's permission-gate.ts.
Tokenizing is quote-aware first (shlex in punctuation mode), and only then split at the
separators `;`, `|`, `&&`, `(`, `)`: a quoted word that merely *contains* a separator and a
git verb (a grep pattern, a commit message) stays one word and is not a command. `$(...)`
and backtick bodies are scanned on their own and then collapsed, so `git -C $(pwd) push`
still parses as a push. `sh -c '...'` and `eval` bodies recurse.
Deliberate gaps: heredoc bodies are skipped -- they are file/script *contents*, and a
README that documents pushing must stay writable -- so a script piped in through a heredoc
is not inspected. Aliases (`git ci`) are not expanded. The deny list, the auto-mode
classifier and the global CLAUDE.md still cover those; this is the belt for the prefix
rules' braces. Fails open: any parse error lets the command through (exit 0).
"""
import json
import os
import re
import shlex
import sys
GIT_BLOCK = {"commit", "push", "clean", "revert", "cherry-pick", "rebase"}
MERGE_OK = {"--abort", "--quit", "--continue"}
STASH_BLOCK = {"drop", "pop", "clear"}
BRANCH_FORCE = {"-D", "-f", "--force", "-M"}
RESET_MODES = {"--hard", "--soft", "--merge", "--keep"}
UNSTAGE_ALL = {".", ":/", "*", ":"}
_HEX = re.compile(r"^[0-9a-f]{7,40}$")
def looks_like_ref(a):
return (a in ("HEAD", "ORIG_HEAD", "FETCH_HEAD", "MERGE_HEAD") or a.startswith("refs/")
or "~" in a or "^" in a or "@{" in a or bool(_HEX.match(a)))
GH_PR_BLOCK = {"merge", "create", "close", "edit"}
# global options that take a *separate* argument; the `--opt=value` spelling is one token.
GIT_OPT_ARG = {"-C", "-c", "--git-dir", "--work-tree", "--exec-path", "--namespace",
"--super-prefix", "--config-env"}
GH_OPT_ARG = {"-R", "--repo"}
SHELLS = {"sh", "bash", "dash", "zsh", "ksh", "fish"}
# `<<'EOF' ... EOF` / `<<-EOF ... EOF`: drop the body, keep the command around it.
_HEREDOC = re.compile(r"<<-?\s*(['\"]?)(\w+)\1[^\n]*\n.*?\n[ \t]*\2[ \t]*(?=\n|$)", re.S)
# innermost `$(...)` / `...` command substitutions (peeled repeatedly for nesting).
_SUBST = re.compile(r"\$\(([^()]*)\)|`([^`]*)`")
# tokens that end one simple command and start the next.
_SEP = {";", ";;", "|", "||", "|&", "&", "&&", "(", ")"}
def _skip_opts(args, with_arg):
j = 0
while j < len(args) and args[j].startswith("-"):
j += 2 if args[j] in with_arg else 1
return j
def git_reason(args):
j = _skip_opts(args, GIT_OPT_ARG)
if j >= len(args):
return None
sub, rest = args[j], args[j + 1:]
if sub in GIT_BLOCK:
return f"git {sub}"
if sub == "reset":
opts = [a for a in rest if a.startswith("-") and a != "--"]
if RESET_MODES & set(opts):
return "git reset --hard/--soft/--merge/--keep (moves HEAD)"
if "-p" in opts or "--patch" in opts or "--" in rest:
return None # interactive, or the explicit path form
pos = [a for a in rest if not a.startswith("-")]
if not pos or pos == ["HEAD"]:
return "git reset (unstages everything, other sessions' rounds included)"
if len(pos) == 1 and (looks_like_ref(pos[0]) or not os.path.exists(pos[0])):
return "git reset <commit> (moves HEAD)"
return None # `reset HEAD <paths>` / `reset <paths>`
if sub == "merge" and not (MERGE_OK & set(rest)):
return "git merge (creates a commit)"
if sub == "restore":
staged = any(a in ("--staged", "-S") for a in rest)
worktree = any(a in ("--worktree", "-W") for a in rest)
if not staged or worktree:
return "git restore (discards working-tree changes)"
if UNSTAGE_ALL & {a for a in rest if not a.startswith("-")}:
return "git restore --staged . (unstages everything, other sessions' rounds included)"
if sub == "checkout" and ("--" in rest or "." in rest or "-f" in rest or "--force" in rest):
return "git checkout -- <paths> (discards working-tree changes)"
if sub == "stash" and rest and rest[0] in STASH_BLOCK:
return f"git stash {rest[0]}"
if sub == "branch" and BRANCH_FORCE & set(rest):
return "git branch -D/-f (force-deletes or force-moves a branch)"
return None
def gh_reason(args):
j = _skip_opts(args, GH_OPT_ARG)
if j >= len(args) or args[j] != "pr":
return None
rest = args[j + 1:]
k = _skip_opts(rest, GH_OPT_ARG)
if k < len(rest) and rest[k] in GH_PR_BLOCK:
return f"gh pr {rest[k]}"
return None
def tokenize(text):
"""Quote-aware shell words; the separator characters come out as their own tokens
(shlex punctuation mode). Comment handling is off: a `#` mid-word must not swallow
the rest of the line."""
lex = shlex.shlex(text, posix=True, punctuation_chars=True)
lex.whitespace_split = True
lex.commenters = ""
try:
return list(lex)
except ValueError: # unbalanced quotes: best effort
return text.split()
def segments(toks):
seg = []
for t in toks:
if t in _SEP:
if seg:
yield seg
seg = []
else:
seg.append(t)
if seg:
yield seg
def scan(text, depth=0):
"""Reason string for the first blocked invocation in `text`, else None."""
if depth > 4:
return None
text = _HEREDOC.sub(" ", text)
# command substitutions: scan each body on its own, then collapse it to a plain word so
# the enclosing command still parses (`git -C $(pwd) push` -> `git -C SUBST push`).
bodies = []
def _grab(m):
bodies.append(m.group(1) if m.group(1) is not None else m.group(2))
return " SUBST "
prev = None
while prev != text:
prev, text = text, _SUBST.sub(_grab, text)
for body in bodies:
r = scan(body, depth + 1)
if r:
return r
for toks in segments(tokenize(text)):
for i, tok in enumerate(toks):
base = os.path.basename(tok)
r = None
if base == "git":
r = git_reason(toks[i + 1:])
elif base == "gh":
r = gh_reason(toks[i + 1:])
elif base == "eval":
r = scan(" ".join(toks[i + 1:]), depth + 1)
elif base in SHELLS and "-c" in toks[i + 1:]:
k = toks.index("-c", i + 1) # `sh -c '<cmd>'`: the real command is one word
if k + 1 < len(toks):
r = scan(toks[k + 1], depth + 1)
if r:
return r
return None
def main():
try:
data = json.load(sys.stdin)
if data.get("tool_name", "Bash") != "Bash":
return
cmd = (data.get("tool_input") or {}).get("command") or ""
reason = scan(cmd) if cmd else None
except Exception:
return # fail open: the deny list still applies
if reason:
short = cmd.strip().splitlines()[0]
short = short if len(short) <= 80 else short[:77] + "..."
sys.stderr.write(
f"git-guard: blocked `{short}` ({reason}). The user commits, pushes and merges "
"manually (global CLAUDE.md): stage and stop, and draft the message with /commit-msg.\n"
)
sys.exit(2)
if __name__ == "__main__":
main()
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""UserPromptExpansion hook: run a skill TUI-side, without a model turn.
Custom slash commands in Claude Code are prompts -- typing `/x` expands skills/x/SKILL.md
and sends it to the model. For a command that is really just a local action (flip a
setting, print a state) that round trip is waste. This hook gives any skill a TUI-side
implementation: if `~/.claude/skills/<command_name>/local.py` exists, it is executed with
the command's arguments, its output is shown to you, and the expansion is blocked (exit 2),
so nothing reaches the model. Skills without a local.py expand as usual (exit 0).
Order matters: the hook fires AFTER the skill body has been expanded, so any `!` inline
commands in that SKILL.md have already run by the time this executes. A skill with a
local.py must therefore not also apply its action via `!` -- it would happen twice.
Input (stdin JSON): command_name, command_args (list), expanded_prompt, cwd, ...
Fails open: any error here means exit 0 and the normal expansion goes ahead.
"""
import json
import os
import subprocess
import sys
def main():
try:
data = json.load(sys.stdin)
name = data.get("command_name") or ""
args = data.get("command_args") or []
if isinstance(args, str):
args = args.split()
if not name or "/" in name or name.startswith("."):
return
script = os.path.expanduser(f"~/.claude/skills/{name}/local.py")
if not os.path.isfile(script):
return
r = subprocess.run(
[sys.executable, script, *[str(a) for a in args]],
capture_output=True, text=True, timeout=20, cwd=data.get("cwd") or None,
)
except Exception:
return # fail open: let the skill expand normally
out = (r.stdout or "") + (r.stderr or "")
sys.stderr.write(out.rstrip("\n") + f"\n[/{name}: handled TUI-side by skills/{name}/local.py, no model turn]\n")
sys.exit(2)
if __name__ == "__main__":
main()
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Notification hook: a desktop notification when Claude is waiting on you.
settings.json wires it to `idle_prompt|permission_prompt` (the Notification matcher is the
`notification_type`). Push notifications are off in this setup, so this is the local
counterpart of pi's notify.ts. Uses notify-send where present (the Wayland/GUI hosts) and
silently no-ops on a headless box or without a session bus. Payload fields are read
defensively -- the docs do not pin the Notification input schema: `message`, `title`,
`notification_type`, `cwd`.
"""
import json
import os
import shutil
import subprocess
import sys
TITLES = {
"idle_prompt": "Claude Code is waiting for you",
"permission_prompt": "Claude Code needs a permission",
}
def main():
try:
data = json.load(sys.stdin)
except Exception:
return
exe = shutil.which("notify-send")
if not exe:
return
kind = data.get("notification_type") or "notification"
title = data.get("title") or TITLES.get(kind, "Claude Code")
body = (data.get("message") or "").strip() or kind
cwd = data.get("cwd") or ""
if cwd:
home = os.path.expanduser("~")
body += "\n" + (("~" + cwd[len(home):]) if cwd.startswith(home) else cwd)
try:
subprocess.run(
[exe, "--app-name=Claude Code", "--urgency=normal", "--expire-time=8000",
# same tag -> a newer notification replaces the previous one instead of stacking
f"--hint=string:x-canonical-private-synchronous:claude-code-{kind}",
title, body],
timeout=5, capture_output=True,
)
except Exception:
pass
if __name__ == "__main__":
main()
+1 -2
View File
@@ -13,8 +13,7 @@
"bindings": {
"alt+j": "scroll:lineDown",
"alt+k": "scroll:lineUp",
"ctrl+shift+j": "scroll:lineDown",
"ctrl+shift+k": "scroll:lineUp"
"ctrl+x v": "app:toggleTranscript"
}
},
{
+81 -3
View File
@@ -13,6 +13,14 @@
"Bash(git reset --hard:*)",
"Bash(git clean)",
"Bash(git clean:*)",
"Bash(gh pr merge)",
"Bash(gh pr merge:*)",
"Bash(gh pr create)",
"Bash(gh pr create:*)",
"Bash(gh pr close)",
"Bash(gh pr close:*)",
"Bash(gh pr edit)",
"Bash(gh pr edit:*)",
"Read(**/.env)",
"Read(**/.env.*)",
"Read(**/*.env)",
@@ -30,15 +38,63 @@
"Read(**/.claude/.credentials.json)",
"Read(**/secrets.fish)",
"Read(**/*.token)",
"Read(**/.envrc)"
"Read(**/.envrc)",
"Read(**/.gnupg/**)",
"Read(**/fish_variables)"
],
"defaultMode": "auto"
},
"model": "claude-fable-5[1m]",
"model": "claude-fable-5-1[1m]",
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "f=\"$HOME/.claude/hooks/drift-check.sh\"; if [ -f \"$f\" ]; then exec bash \"$f\"; fi",
"timeout": 5
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "f=\"$HOME/.claude/hooks/drift-check.sh\"; if [ -f \"$f\" ]; then out=$(bash \"$f\" 2>&1); [ -n \"$out\" ] && printf \"%s\\n\" \"$out\"; fi; exit 0",
"timeout": 5
}
]
}
],
"UserPromptExpansion": [
{
"hooks": [
{
"type": "command",
"command": "f=\"$HOME/.claude/hooks/local-commands.py\"; if [ -f \"$f\" ]; then exec /usr/bin/env python3 \"$f\"; fi",
"timeout": 25
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "f=\"$HOME/.claude/hooks/git-guard.py\"; if [ -f \"$f\" ]; then exec /usr/bin/env python3 \"$f\"; fi",
"timeout": 5
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
@@ -47,6 +103,28 @@
"statusMessage": "format"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/usr/bin/env python3 ~/.claude/hooks/format.py",
"timeout": 20
}
]
}
],
"Notification": [
{
"matcher": "idle_prompt|permission_prompt",
"hooks": [
{
"type": "command",
"command": "f=\"$HOME/.claude/hooks/notify.py\"; if [ -f \"$f\" ]; then exec /usr/bin/env python3 \"$f\"; fi",
"timeout": 5
}
]
}
]
},
@@ -2,7 +2,7 @@
description: Draft a commit message for the staged changes, matching this repo's commit style, then print it in the reply, save it to the repo's CLAUDE_COMMIT_MSG file, and copy it to the clipboard. Never stages or commits. Use once you've staged what you want to commit.
argument-hint: [optional focus/scope hint]
disable-model-invocation: true
allowed-tools: Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git rev-parse:*), Bash(wl-copy:*), Read, Glob, Write
allowed-tools: Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git rev-parse:*), Bash(wl-copy:*), Bash(~/.config/git/claude-commit-msg:*), Read, Glob
---
# Draft a commit message for the staged changes
@@ -29,14 +29,20 @@ Context for the current commit — what's staged, plus this repo's recent style:
ready. This in-reply copy is the durable one: it lives in the transcript, so it survives even if
the clipboard gets overwritten later (by me copying something else, or another Claude session
running `wl-copy`). Always print it; don't rely on the clipboard alone.
5. **Save it to a file in the repo** so I can recover it any time: resolve the path with
`git rev-parse --git-path CLAUDE_COMMIT_MSG` (per-worktree correct), then use the Write tool to
write the raw message there — **not** `COMMIT_EDITMSG`, which git overwrites on every
`git commit` before the editor opens. My `prepare-commit-msg` hook prefills the commit editor
from `CLAUDE_COMMIT_MSG` (one-shot) on the next `git commit` / lazygit `C`. The file lives
inside the git dir so it never shows up in `git status`.
6. **Also copy it to the clipboard** as a convenience: `wl-copy < <path from step 5>`. Then
tell me all three places it is: **printed above**, **saved to `CLAUDE_COMMIT_MSG`** (hook
prefills the next commit), and **on the clipboard** — so I never lose it to a stray copy.
5. **Hand it off** so I can recover it any time — feed the raw message to the CLI as a quoted
heredoc (so quotes, backticks and `$` in the body survive untouched):
```sh
~/.config/git/claude-commit-msg save <<'EOF'
<message>
EOF
```
That stores it as its own file under `$(git rev-parse --git-path CLAUDE_COMMIT_MSG.d)/` **together with the staged paths
it describes**, so several sessions can hand off in parallel without overwriting each other. My
`prepare-commit-msg` hook prefills the next `git commit` / lazygit `C` with the message(s) whose
paths are part of that commit, then consumes them. Never write `CLAUDE_COMMIT_MSG` or
`COMMIT_EDITMSG` directly. The `save` output names the saved id and file path.
6. **Also copy it to the clipboard** as a convenience: `wl-copy < <file path printed by save>`.
Then tell me all three places it is: **printed above**, **saved for the hook** (with its id),
and **on the clipboard** — so I never lose it to a stray copy.
7. **Never** run `git add`, `git commit`, or `git push`, and never offer to — I stage and commit
manually. Draft only.
+19 -14
View File
@@ -2,7 +2,7 @@
description: Babysit the current branch's PR — address review comments that don't need my input, re-check every ~10 min, and stop when there are no open comments left or the automated reviewers (Codex, Claude) hit their limit. Prep-only — stages fixes and drafts the commit message; never commits, pushes, or merges.
argument-hint: [pr number or url — optional; defaults to the current branch's PR]
disable-model-invocation: true
allowed-tools: Bash(gh:*), Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(git rev-parse:*), Bash(git fetch:*), Bash(git add:*), Bash(wl-copy:*), Read, Edit, Write, Grep, Glob, ScheduleWakeup
allowed-tools: Bash(gh:*), Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(git rev-parse:*), Bash(git fetch:*), Bash(git add:*), Bash(wl-copy:*), Bash(~/.config/git/claude-commit-msg:*), Read, Edit, Write, Grep, Glob, ScheduleWakeup
---
# Watch this PR and resolve review comments (prep-only)
@@ -36,11 +36,12 @@ comments left or the automated reviewers (Codex/Claude) hit a usage limit.
session stays open**. **To stop it: press `Esc` while I'm idle between cycles** (that clears the
queued wake-up); closing the session also stops it. A plain message does **not** cancel a pending
wake-up — use `Esc`.
- **Every token counts.** Each wake-up re-reads the whole conversation with a cold prompt cache
(the 10-min cadence outlives the 5-min cache TTL), so cost compounds with every cycle and every
extra line in context. Therefore: fetch **filtered** data only (use the `--jq` filters below —
never let raw unfiltered JSON into the conversation), summarize deltas rather than restating
unchanged state, and keep idle cycles to **one cheap API call and ≤2 lines of output**.
- **Every token counts.** Each wake-up re-sends the whole conversation. On a Claude subscription
the prompt cache holds for **1 hour**, so the 10-min cadence stays warm; on an API key it drops to
5 min and every cycle is a cold read. Either way every extra line in context rides along on every
later cycle. Therefore: fetch **filtered** data only (use the `--jq` filters below — never let raw
unfiltered JSON into the conversation), summarize deltas rather than restating unchanged state,
and keep idle cycles to **one cheap API call and ≤2 lines of output**.
Current state (auth + the current branch's PR):
@@ -233,10 +234,11 @@ Current state (auth + the current branch's PR):
9. **Draft the commit message — exactly like `/commit-msg`:** from the staged diff and the repo's
recent `git log` style (this repo uses `[Scope] summary`), write a message that matches. Then:
- **(a) Print it** in your reply as a fenced ` ```text ` block — the durable copy.
- **(b) Write it** with the Write tool to the path from `git rev-parse --git-path CLAUDE_COMMIT_MSG`
(**not** `COMMIT_EDITMSG`git overwrites that on `git commit`; the `prepare-commit-msg` hook
prefills the editor from `CLAUDE_COMMIT_MSG`).
- **(c) Copy it:** `wl-copy < <that path>`.
- **(b) Hand it off:** `~/.config/git/claude-commit-msg save <<'EOF'``EOF` (the message as a
quoted heredoc, so quotes and backticks survive) — it is stored with the paths you staged this cycle, so the `prepare-commit-msg` hook prefills it only
for the commit that contains them (other sessions' handoffs are not overwritten). Never write
`CLAUDE_COMMIT_MSG` / `COMMIT_EDITMSG` directly.
- **(c) Copy it:** `wl-copy < <file path printed by save>`.
- Tell me all three locations. **Never** run `git commit`.
(Skip this step when nothing new was staged this cycle.)
@@ -252,9 +254,10 @@ Current state (auth + the current branch's PR):
needs-input questions are posted (reactions don't bump `updatedAt`, comments do — so your own
activity doesn't defeat the next cheap check), then save state
to `<git-dir>/pr-loop-state.json`. Then schedule:
- **normal cycle:** `ScheduleWakeup(delaySeconds=600, prompt="/pr-loop", reason="recheck PR
#<num> review comments")`;
- **idle cycle** (step 3 short-circuited): back off — `delaySeconds=1200`, I'm clearly away;
- **normal cycle:** `ScheduleWakeup(delaySeconds=600, noop=false, prompt="/pr-loop",
reason="recheck PR #<num> review comments")`;
- **idle cycle** (step 3 short-circuited): back off — `delaySeconds=1200, noop=true` (idle
ticks then collapse into one line in my terminal instead of stacking), I'm clearly away;
re-running `/pr-loop` checks immediately.
End the turn with a sign-off that states when you'll run again and how to stop, e.g.:
@@ -268,7 +271,9 @@ Current state (auth + the current branch's PR):
- **Never** `git commit`, `git push`, `gh pr merge`, `gh pr create`, `gh pr close`, or `gh pr edit`
— I do all of those. You stage, react, resolve, and draft; nothing more. The only comment you may
post is a needs-input decision question.
post is a needs-input decision question. This is enforced, not just asked: `settings.json` denies
those commands and the `git-guard.py` PreToolUse hook blocks any spelling of them, so a slip is
refused with a reason rather than executed.
- Act only on the target PR. Don't touch unrelated files or other PRs.
- **Keep context lean:** no raw JSON dumps in replies, no re-listing threads that haven't changed,
idle cycles ≤2 lines. Every line you emit is re-read (uncached) on every later cycle.
@@ -2,7 +2,7 @@
description: Review all changes on the current branch (diff vs its base) for bugs, security issues, optimizations, and readability/scannability, then apply the readability + safe improvements after you approve. Use right before opening a PR.
argument-hint: [base-branch]
disable-model-invocation: true
allowed-tools: Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git merge-base:*), Bash(git symbolic-ref:*), Bash(git rev-parse:*), Bash(git show-ref:*), Bash(git branch:*), Read, Grep, Glob
allowed-tools: Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git merge-base:*), Bash(git symbolic-ref:*), Bash(git rev-parse:*), Bash(git show-ref:*), Bash(git branch:*), Read, Grep, Glob, Edit, Write
---
# Review this branch
@@ -59,11 +59,10 @@ Context auto-collected for the current branch vs. its base:
surfaced, not a blanket dump. Only include the ones that actually fit:
- `/security-review` — a deeper, whole-branch security pass; suggest whenever the 🔒 lens
flagged something or the diff touches auth, crypto, input handling, secrets, or network I/O.
- `/code-review` (add `--fix` to apply, `--comment` for inline PR comments) — a second review
of the working-tree diff at a chosen effort level.
- `/code-review` (alias `/review`; add `--fix` to apply, `--comment` for inline PR comments) —
a second review of the working-tree diff, a PR number or a branch at a chosen effort level.
- `/simplify` — apply reuse / simplification / efficiency cleanups beyond the safe ones applied
here.
- `/verify` — run the app to confirm the change behaves, if this branch changed behavior.
- `/pr-description` — draft the PR text from this branch once the review is clean.
- `/review` — review the PR on GitHub after it's open.
8. **Never** run `git commit` or `git push` — I do that manually.
@@ -0,0 +1,20 @@
---
description: Toggle the persisted verbose view for this project (viewMode verbose/default, plus verbose + showThinkingSummaries) by writing .claude/settings.local.json, and print the live-view keybind. Usage: /verbose [on|off|status]; no argument toggles. Runs TUI-side via the UserPromptExpansion hook; this body is only the fallback.
argument-hint: [on|off|status]
disable-model-invocation: true
allowed-tools: Bash(python3:*)
---
# /verbose (fallback path)
Normally you never see this: `hooks/local-commands.py` (UserPromptExpansion) runs
`skills/verbose/local.py` and blocks the expansion, so `/verbose` costs no model turn. This body
only loads on a host where that hook isn't deployed yet.
Run exactly this, then relay its output to me verbatim in a fenced ```text block with **at most one
line** of your own:
python3 ~/.claude/skills/verbose/local.py $ARGUMENTS
Don't edit any settings file yourself, don't run anything else, and don't offer follow-ups. If it
reports an error, quote it and stop.
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""`/verbose`, TUI-side: flip Claude Code's *persisted* verbose view for the current project.
Run by hooks/local-commands.py from the UserPromptExpansion hook, i.e. when you type
`/verbose [on|off|status]` -- the hook executes this and blocks the skill expansion, so no
prompt ever reaches the model and the output below is shown to you directly. (On a host
without that hook, skills/verbose/SKILL.md falls back to having the model run it.)
Claude Code has no built-in runtime `/verbose`; the live toggle is a keypress (`ctrl+x v`
here, `ctrl+o` by default -- `app:toggleTranscript`). What can be scripted is the persisted
default: `viewMode` ("verbose" | "default" | "focus" -- this is what actually decides the view;
it also beats a sticky `/focus` choice for new sessions), plus `verbose` (full tool
input/output inline) and `showThinkingSummaries` for versions without viewMode. All accept any
settings scope, so this writes the project's
`.claude/settings.local.json` -- local to this machine, never committed -- instead of the
dots-tracked ~/.claude/settings.json, which would show a diff on every toggle. Project-local
settings win over user settings, so the user default stays `verbose: true` and a project can
be quietened individually.
usage: local.py [on|off|toggle|status] (no argument = toggle)
"""
import json
import os
import subprocess
import sys
KEYS = ("verbose", "showThinkingSummaries")
VIEW = "viewMode" # "verbose" | "default" | "focus"; overrides KEYS and the sticky /focus choice
LIVE = "live view: ctrl+x v (or ctrl+o) toggles the transcript; ctrl+e inside it shows everything"
def project_root():
try:
r = subprocess.run(["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True, timeout=2)
if r.returncode == 0 and r.stdout.strip():
return r.stdout.strip()
except Exception:
pass
return os.getcwd()
def load(path):
try:
with open(path) as fh:
return json.load(fh)
except FileNotFoundError:
return {}
except Exception as e: # a broken local file must not be silently overwritten
sys.exit(f"error: cannot parse {path}: {e}")
def main():
raw = " ".join(sys.argv[1:]).strip()
mode = raw.lower() or "toggle"
if mode not in ("on", "off", "toggle", "status"):
sys.exit("usage: /verbose [on|off|status]")
root = project_root()
local_path = os.path.join(root, ".claude", "settings.local.json")
user = load(os.path.expanduser("~/.claude/settings.json"))
local = load(local_path)
effective = {k: bool(local.get(k, user.get(k, False))) for k in KEYS}
view = local.get(VIEW, user.get(VIEW))
def state_of(view, eff):
if view == "verbose":
return "ON"
if view in ("default", "focus"):
return f"OFF ({view} forced by settings)"
if all(eff.values()):
return "ON via the verbose key (viewMode unset: a sticky /focus choice still wins)"
return "OFF" if not any(eff.values()) else "MIXED"
if mode != "status":
current_on = state_of(view, effective).startswith("ON")
on = {"on": True, "off": False}.get(mode, not current_on)
if on:
local[VIEW] = "verbose" # forces verbose for new sessions, beating a sticky /focus
else:
local.pop(VIEW, None) # release: the sticky /focus choice (or default) applies again
for k in KEYS:
local[k] = on
os.makedirs(os.path.dirname(local_path), exist_ok=True)
with open(local_path, "w") as fh:
json.dump(local, fh, indent=2)
fh.write("\n")
effective = {k: on for k in KEYS}
view = local.get(VIEW, user.get(VIEW))
print(f"verbose view: {'ON' if on else 'OFF'} for {root}")
print(f" written to {local_path} (viewMode applies to new sessions; the current one: ctrl+x v)")
else:
print(f"verbose view: {state_of(view, effective)} for {root}")
print(f" {VIEW}: {view or '-'} (project-local: {local.get(VIEW, '-')}, user: {user.get(VIEW, '-')})")
for k in KEYS:
print(f" {k}: {str(effective[k]).lower()} (project-local: {local.get(k, '-')}, user: {user.get(k, '-')})")
print(LIVE)
if __name__ == "__main__":
main()
+52 -17
View File
@@ -3,7 +3,8 @@
One line: vim mode | dir (repo-relative, hidden at the root) |
git(repo/branch +dirty +ahead/behind) | model (+effort) |
context% (+compact warn) | 5h usage (+reset eta) | cost (+burn rate) | disk. If the
context% (+compact warn) | 5h usage (+reset eta) | cost (+burn rate) |
prompt-cache hit% (+ttl, cold marker) | disk. If the
rendered line would overflow the terminal, trailing (lowest-priority) segments are
dropped until it fits.
(Dormant, re-addable in main(): ram, cpu%, temp -- retired 2026-07-26 because the
@@ -89,7 +90,24 @@ def term_cols():
return 0
# --- shared transcript usage ------------------------------------------------
# --- context usage ------------------------------------------------------------
_USAGE_KEYS = ("input_tokens", "cache_creation_input_tokens", "cache_read_input_tokens")
def payload_usage(data):
"""`context_window.current_usage` from the status payload, shaped like `message.usage`.
Claude Code 2.1.x hands the status line its own token accounting, so the transcript
scan below is only a fallback for payloads that predate it (or lack a transcript).
"""
cw = data.get("context_window")
cu = cw.get("current_usage") if isinstance(cw, dict) else None
if isinstance(cu, dict) and any(isinstance(cu.get(k), (int, float)) for k in _USAGE_KEYS):
return cu
return {}
# --- shared transcript usage (fallback) ------------------------------------------
def _scan_usage(text):
"""Last `message.usage` block among these JSONL lines, or {}."""
usage = {}
@@ -113,7 +131,7 @@ TRANSCRIPT_TAIL = 1 << 20 # 1 MiB
def last_usage(data):
"""Most recent `message.usage` block from the transcript, or {}.
Reads only the tail: this runs on every status refresh and transcripts grow to
Fallback only -- see payload_usage(). Reads only the tail: this runs on every status refresh and transcripts grow to
tens of MB, so re-parsing the whole file each time is unbounded work for a value
that lives at the end. Falls back to a full scan if the tail holds no usage block
(a long stretch of tool results can push it past the window).
@@ -292,6 +310,11 @@ def context_segment(data, usage):
)
window = context_window(data)
pct = used / window * 100 if window else 0
# Prefer Claude Code's own percentage when the payload carries it (2.1.x) -- it
# already accounts for the auto-compact reserve and window quirks we can't see.
cw = data.get("context_window")
if isinstance(cw, dict) and isinstance(cw.get("used_percentage"), (int, float)):
pct = float(cw["used_percentage"])
col = bucket(pct, 50, 80)
out = "📝 " + color(f"{pct:.0f}%", col) + color(f" ({used / 1000:.0f}k)", "gray", dim=True)
if pct >= 80:
@@ -353,20 +376,31 @@ def velocity_segment(data):
return ""
def cache_segment(usage):
def cache_segment(usage, data=None):
"""Prompt-cache hit % (higher is better) + the cache TTL, `·cold` when the cache has
expired. Hit % from the 2.1.260+ payload `prompt_cache.hit_ratio` (a 0..1 fraction, null
while all counts are zero), else computed from the usage block; ttl/cold always from the
payload when present. A 1h TTL is what keeps a 10-minute pr-loop cadence warm."""
try:
if not usage:
pc = (data or {}).get("prompt_cache")
pc = pc if isinstance(pc, dict) else {}
pct = None
if isinstance(pc.get("hit_ratio"), (int, float)):
pct = float(pc["hit_ratio"]) * 100
elif usage:
read = usage.get("cache_read_input_tokens", 0)
total = usage.get("input_tokens", 0) + usage.get("cache_creation_input_tokens", 0) + read
if total > 0:
pct = read / total * 100
if pct is None:
return ""
read = usage.get("cache_read_input_tokens", 0)
total = (
usage.get("input_tokens", 0)
+ usage.get("cache_creation_input_tokens", 0)
+ read
)
if total <= 0:
return ""
pct = read / total * 100
return "♻️ " + color(f"{pct:.0f}%", bucket(pct, 50, 80, invert=True))
out = "♻️ " + color(f"{pct:.0f}%", bucket(pct, 50, 80, invert=True))
ttl = pc.get("ttl")
if isinstance(ttl, str) and ttl:
out += color(f" {ttl}", "gray")
if pc.get("warm") is False:
out += color(" ·cold", "yellow")
return out
except Exception:
return ""
@@ -532,7 +566,7 @@ def main():
or data.get("cwd")
or os.getcwd()
)
usage = last_usage(data)
usage = payload_usage(data) or last_usage(data)
top = git_top(cwd)
# One row: work (left) then system (right), joined. If it would overflow the
@@ -540,7 +574,7 @@ def main():
# Dormant helpers kept above for easy re-add: ram_segment, cpu_segment,
# temp_segment (retired -- they only refresh on message events, so they sat
# stale; the tmux status bar owns system metrics now), velocity_segment,
# cache_segment, api_segment, version_segment, style_segment.
# api_segment, version_segment, style_segment.
work = [s for s in (
vim_segment(data),
dir_segment(cwd, top),
@@ -549,6 +583,7 @@ def main():
context_segment(data, usage),
usage_segment(data),
cost_segment(data),
cache_segment(usage, data),
) if s]
system = [s for s in (
disk_segment(cwd),
+6 -1
View File
@@ -5,7 +5,11 @@ set -gx EDITOR nvim
set -gx PAGER less
set -gx QT_FONT_DPI 96
set -gx PI_SKIP_VERSION_CHECK 1
set -gx OPENCODE_CONFIG "$HOME/.config/opencode/openai-gpt.jsonc"
# opencode GPT/Codex OAuth overlay — only where `opencode auth login` stored an OpenAI credential,
# so hosts without the login don't get an unusable provider in the model picker.
if test -r ~/.local/share/opencode/auth.json; and grep -q '"openai"' ~/.local/share/opencode/auth.json
set -gx OPENCODE_CONFIG "$HOME/.config/opencode/openai-gpt.jsonc"
end
set -gx MANPAGER "nvim +Man!"
# fzf's built-in walker is the one search tool that ignores .ignore files, so point it at
# fd, which honours them. fd still hides dotfiles by default everywhere else — this only
@@ -48,6 +52,7 @@ if status is-interactive
# Git
alias gti="git"
alias gs="git status -s"
alias gp="git pull"
alias gca="git add -p . && git commit"
alias gd="git diff --word-diff"
alias gl="git log --graph --show-signature"
+29 -5
View File
@@ -1,4 +1,25 @@
function opencodes --description 'contained opencode'
function opencodes --description 'contained opencode (bwrap); extra args are passed to opencode'
set -l cfg ~/.config/opencode
# ~/.config/opencode is stowed as per-file symlinks into ~/.dots. Bind each link's real parent
# dir read-only too, or the sandbox sees dangling links and opencode starts with NO config.
set -l dirs
for f in $cfg/* $cfg/*/*
test -L $f; or continue
set -l d (path resolve $f | path dirname)
string match -q "$cfg*" $d; and continue
contains -- $d $dirs; or set -a dirs $d
end
set -l binds
for d in $dirs
set -a binds --ro-bind $d $d
end
# --clearenv drops everything; pass through only what the TUI and its tools need.
set -l env --setenv PATH /usr/local/bin:/usr/bin:/bin
for v in HOME TERM COLORTERM LANG LC_ALL EDITOR OPENCODE_CONFIG DUSKADIY_API_KEY
set -l val $$v
test -n "$val"; and set -a env --setenv $v $val
end
mkdir -p ~/.cache/opencode ~/.local/state/opencode ~/.local/share/opencode
bwrap \
--unshare-all \
--new-session \
@@ -12,10 +33,13 @@ function opencodes --description 'contained opencode'
--ro-bind /lib64 /lib64 \
--ro-bind /bin /bin \
--ro-bind /etc /etc \
--ro-bind ~/.config/opencode ~/.config/opencode \
--tmpfs /tmp/node-compile-cache \
--ro-bind $cfg $cfg \
$binds \
--tmpfs /tmp \
--bind ~/.cache/opencode ~/.cache/opencode \
--bind ~/.local/state/opencode ~/.local/state/opencode \
--bind ~/.local/share/opencode ~/.local/share/opencode \
--bind "$(pwd)" "$(pwd)" \
opencode
--bind $PWD $PWD \
$env \
opencode $argv
end
+256
View File
@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""claude-commit-msg -- hand suggested commit messages from Claude sessions to `git commit`.
Several Claude sessions work on one checkout at once, each staging its own round. The old
handoff was ONE file (`<git-dir>/CLAUDE_COMMIT_MSG`) that the prepare-commit-msg hook pasted
into the editor: the second session silently overwrote the first, and the hook could not
know which message described which files. Now every handoff is its own file under
`<git-dir>/CLAUDE_COMMIT_MSG.d/`, tagged with the paths it describes, and the hook prefills
only the message(s) whose paths are part of the commit being made:
<id>.msg the raw message
<id>.paths one repo-relative path per line (the staged set at save time)
used/ consumed messages, kept for recovery
save [--paths P ...] [--slug S] read the message on stdin; paths default to the index
list pending messages and how they match the current index
show ID | rm ID
prefill MSGFILE [SOURCE] hook entry point (prepare-commit-msg)
Matching in `prefill`: a message whose paths are all in the commit is a FULL match (prefilled,
then consumed); overlapping only partly = PARTIAL (prefilled, kept -- the rest may come in a
later commit); no paths recorded = WILDCARD (prefilled, consumed), which is also how a legacy
single CLAUDE_COMMIT_MSG file is treated. Several matches are all prefilled, separated by a
comment line git strips, so you pick in the editor. Nothing matching = editor untouched.
Everything lives inside the git dir, so it never shows in `git status`; `--git-path` keeps it
per-worktree. Always exits 0 from `prefill`: a hook failure must never abort a commit.
"""
import os
import re
import shutil
import subprocess
import sys
import time
def git(*args, check=True):
r = subprocess.run(["git", *args], capture_output=True, text=True)
if check and r.returncode != 0:
sys.exit(f"claude-commit-msg: git {' '.join(args)} failed: {r.stderr.strip()}")
return r.stdout
def store():
d = git("rev-parse", "--git-path", "CLAUDE_COMMIT_MSG.d").strip()
d = os.path.abspath(d)
os.makedirs(os.path.join(d, "used"), exist_ok=True)
return d
def staged_paths():
out = git("diff", "--cached", "--name-only", "-z", check=False)
return [p for p in out.split("\0") if p]
def comment_char():
c = git("config", "--get", "core.commentchar", check=False).strip()
return "#" if not c or c == "auto" else c[0]
def pending(d):
"""[(id, message, paths|None)] oldest first. paths None = wildcard."""
out = []
for name in sorted(os.listdir(d)):
if not name.endswith(".msg"):
continue
mid = name[:-4]
try:
with open(os.path.join(d, name)) as fh:
msg = fh.read()
except OSError:
continue
paths = None
pf = os.path.join(d, mid + ".paths")
if os.path.exists(pf):
with open(pf) as fh:
paths = [ln.rstrip("\n") for ln in fh if ln.strip()]
out.append((mid, msg, paths))
return out
def slugify(text):
m = re.match(r"\s*\[([^\]]+)\]", text) # this repo's `[Scope] summary` convention
base = m.group(1) if m else " ".join(text.split()[:3])
s = re.sub(r"[^a-z0-9]+", "-", base.lower()).strip("-")
return s[:32] or "msg"
def cmd_save(argv):
paths, slug = None, None
i = 0
while i < len(argv):
if argv[i] == "--paths":
paths = []
i += 1
while i < len(argv) and not argv[i].startswith("--"):
paths.append(argv[i])
i += 1
continue
if argv[i] == "--slug" and i + 1 < len(argv):
slug = argv[i + 1]
i += 2
continue
sys.exit(f"claude-commit-msg save: unknown argument {argv[i]!r}")
msg = sys.stdin.read().rstrip() + "\n"
if not msg.strip():
sys.exit("claude-commit-msg save: empty message on stdin")
if paths is None:
paths = staged_paths()
d = store()
mid = time.strftime("%Y%m%d-%H%M%S") + "-" + (slug or slugify(msg))
n = 1
while os.path.exists(os.path.join(d, f"{mid}.msg")):
n += 1
mid = f"{mid}-{n}"
with open(os.path.join(d, mid + ".msg"), "w") as fh:
fh.write(msg)
with open(os.path.join(d, mid + ".paths"), "w") as fh:
fh.write("".join(p + "\n" for p in paths))
print(f"saved {mid} ({len(paths)} path{'s' if len(paths) != 1 else ''}) -> {os.path.join(d, mid + '.msg')}")
if not paths:
print("warning: no paths recorded (nothing staged?) -- it will prefill on ANY commit", file=sys.stderr)
warn_if_hook_is_old()
def warn_if_hook_is_old():
"""A clone still running the legacy hook (or none) reads only the single file this tool
never writes -- the editor would come up empty with no error. Say so at save time."""
hook = git("rev-parse", "--git-path", "hooks/prepare-commit-msg", check=False).strip()
try:
with open(hook, errors="ignore") as fh:
ok = "claude-commit-msg" in fh.read()
except OSError:
ok = False
if not ok:
print(
"warning: this repo's prepare-commit-msg hook is missing or predates claude-commit-msg, so this\n"
" message will NOT be prefilled here. Install the tracked hook once per repo:\n"
" ln -sf ~/.config/git/hooks/prepare-commit-msg (git rev-parse --git-path hooks)/prepare-commit-msg",
file=sys.stderr,
)
def classify(paths, staged):
if paths is None or not paths:
return "wildcard"
ps = set(paths)
if ps <= staged:
return "full"
if ps & staged:
return "partial"
return "none"
def cmd_list(_argv):
d = store()
staged = set(staged_paths())
items = pending(d)
legacy = git("rev-parse", "--git-path", "CLAUDE_COMMIT_MSG").strip()
if not items and not os.path.exists(legacy):
print("no pending messages")
return
for mid, msg, paths in items:
n = len(paths) if paths else 0
print(f"{mid} [{classify(paths, staged):8}] {n} path{'s' if n != 1 else ''} {msg.splitlines()[0][:70]}")
if os.path.exists(legacy) and os.path.getsize(legacy) > 0:
with open(legacy) as fh:
first = fh.readline().rstrip()
print(f"(legacy CLAUDE_COMMIT_MSG) [wildcard] {first[:70]}")
def cmd_show(argv):
d = store()
for mid in argv:
with open(os.path.join(d, mid + ".msg")) as fh:
sys.stdout.write(fh.read())
def cmd_rm(argv):
d = store()
for mid in argv:
hit = False
for ext in (".msg", ".paths"):
p = os.path.join(d, mid + ext)
if os.path.exists(p):
os.remove(p)
hit = True
if hit:
print(f"removed {mid}")
elif os.path.exists(os.path.join(d, "used", mid + ".msg")):
print(f"{mid} was already consumed by a commit (see used/)")
else:
print(f"{mid}: no such pending message")
def consume(d, mid):
for ext in (".msg", ".paths"):
p = os.path.join(d, mid + ext)
if os.path.exists(p):
shutil.move(p, os.path.join(d, "used", mid + ext))
def cmd_prefill(argv):
if not argv:
return
msg_file = argv[0]
source = argv[1] if len(argv) > 1 else ""
if source not in ("", "template"): # -m/-F, merge, squash, amend: leave alone
return
d = store()
staged = set(staged_paths())
order = {"full": 0, "partial": 1, "wildcard": 2}
picked = []
for mid, msg, paths in pending(d):
kind = classify(paths, staged)
if kind != "none":
picked.append((order[kind], mid, msg, kind))
legacy = git("rev-parse", "--git-path", "CLAUDE_COMMIT_MSG").strip()
legacy_msg = None
if os.path.isfile(legacy) and os.path.getsize(legacy) > 0:
with open(legacy) as fh:
legacy_msg = fh.read()
picked.append((3, "legacy", legacy_msg, "wildcard"))
if not picked:
return
picked.sort(key=lambda t: (t[0], t[1]))
cc = comment_char()
sep = f"\n{cc} ---- another pending Claude message matched this commit; keep the right one ----\n\n"
body = sep.join(m.rstrip("\n") + "\n" for _, _, m, _ in picked)
with open(msg_file) as fh:
existing = fh.read()
tmp = msg_file + ".claude-tmp"
with open(tmp, "w") as fh:
fh.write(body + "\n" + existing)
os.replace(tmp, msg_file)
for _, mid, _, kind in picked:
if mid == "legacy":
os.replace(legacy, legacy + ".last")
elif kind in ("full", "wildcard"):
consume(d, mid)
def main():
cmds = {"save": cmd_save, "list": cmd_list, "show": cmd_show, "rm": cmd_rm, "prefill": cmd_prefill}
if len(sys.argv) < 2 or sys.argv[1] not in cmds:
sys.exit("usage: claude-commit-msg save [--paths P ...] [--slug S] < message | list | show ID | rm ID | prefill MSGFILE [SOURCE]")
if sys.argv[1] == "prefill":
try:
cmd_prefill(sys.argv[2:])
except Exception as e: # never abort a commit because of the helper
print(f"claude-commit-msg prefill: {e}", file=sys.stderr)
return
cmds[sys.argv[1]](sys.argv[2:])
if __name__ == "__main__":
main()
+36
View File
@@ -0,0 +1,36 @@
#!/bin/sh
# Prefill the commit editor from pending Claude commit messages (one-shot).
#
# Claude sessions save suggestions with `~/.config/git/claude-commit-msg save` -- one file per
# message under $(git rev-parse --git-path CLAUDE_COMMIT_MSG.d)/, tagged with the paths it
# describes. On the next `git commit` / lazygit `C` this pastes the message(s) whose paths are
# part of the commit above git's comment block, then consumes them (moved to used/). Several
# sessions can hand off in parallel without overwriting each other.
#
# Falls back to the legacy single file $(git rev-parse --git-path CLAUDE_COMMIT_MSG) when the
# CLI is not deployed on this host (not re-stowed yet). Never exits non-zero: a helper problem
# must not abort a commit.
msg_file=$1
source=$2
# Only prefill a fresh editor session -- skip -m/-F (message), merge, squash, amend (commit).
case "$source" in
""|template) ;;
*) exit 0 ;;
esac
cli="$HOME/.config/git/claude-commit-msg"
if [ -f "$cli" ] && command -v python3 >/dev/null 2>&1; then
python3 "$cli" prefill "$msg_file" "$source"
exit 0
fi
# --- legacy fallback: the single one-shot file ---
suggestion=$(git rev-parse --git-path CLAUDE_COMMIT_MSG)
[ -s "$suggestion" ] || exit 0
tmp=$(mktemp) || exit 0
{ cat "$suggestion"; printf '\n'; cat "$msg_file"; } > "$tmp" &&
mv "$tmp" "$msg_file" &&
mv "$suggestion" "$suggestion.last"
exit 0
+1
View File
@@ -30,6 +30,7 @@ nvim
vim
tetrigo
pyradio
cliamp
px7-radio
kew
cmatrix
+22
View File
@@ -0,0 +1,22 @@
# Global agent instructions
You are a coding assistant running on small, locally-hosted models. Be precise and economical with tokens.
## Working style
- Act with tools instead of describing what you would do. Keep prose short.
- Do the task that was asked. Don't add unrequested changes, refactors, or files.
- When done, stop. A one- or two-line summary is enough; no recaps of obvious steps.
## Files & edits
- Read a file before you edit it. Never guess at file paths, function names, or APIs — verify first.
- Use the edit tool for changes. Make minimal, targeted diffs; never paste an entire file back to the user.
- Match the surrounding code's style, naming, and imports.
## Shell
- Run one command at a time and check its output before the next.
- Prefer `rg` and `fd` for search. Use read-only commands when exploring.
- Never run destructive or system-changing commands (`rm -rf`, `sudo`, package installs, force-push) unless explicitly asked.
## Honesty
- If you're unsure, say so and check rather than inventing an answer.
- Report failures plainly with the actual error; don't claim success you didn't verify.
@@ -0,0 +1,11 @@
---
description: Write a Conventional Commits message for the staged changes (does not commit)
agent: plan
---
Inspect the staged changes with `git diff --cached` (and `git status` for context). Then write a single Conventional Commits message:
- A `<type>(<scope>): <subject>` summary line, imperative mood, ≤72 chars (types: feat, fix, refactor, docs, chore, test, perf, build).
- An optional short body explaining the *why* only when it isn't obvious from the diff.
- Do NOT run `git commit`. Output only the message inside a code block so I can review it.
Extra context from me, if any: $ARGUMENTS
@@ -0,0 +1,14 @@
---
description: Review the current diff (staged + unstaged) for bugs and issues, read-only
agent: plan
---
Review the current changes. Check both `git diff` (unstaged) and `git diff --cached` (staged). Focus on:
- Correctness and logic bugs
- Error handling and missing edge cases
- Security issues (injection, secrets, unsafe input)
- Anything that doesn't match the surrounding code's conventions
List concrete findings as `file:line — issue — suggested fix`, ordered by severity. Be concise; skip praise. If nothing is wrong, say so.
Extra focus this round, if any (default: general correctness): $ARGUMENTS
+8
View File
@@ -14,6 +14,14 @@
],
"store": false
},
"whitelist": [
"gpt-5.2",
"gpt-5.2-codex",
"gpt-5.1-codex-max",
"gpt-5.1-codex",
"gpt-5.1-codex-mini",
"gpt-5.1"
],
"models": {
"gpt-5.2": {
"name": "GPT 5.2 (OAuth)",
+134 -29
View File
@@ -1,10 +1,11 @@
{
"$schema": "https://opencode.ai/config.json",
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"small_model": "llama.cpp/gemma-4-E4B-it-UD-Q8_K_XL",
"instructions": ["CLAUDE.md", "AGENTS.md"],
"share": "disabled",
"autoupdate": "notify",
"tool_output": { "max_lines": 200, "max_bytes": 16384 },
"snapshot": false,
"disabled_providers": [
"gemini",
"anthropic",
@@ -17,6 +18,7 @@
"webfetch": "ask",
"websearch": "ask",
"external_directory": "ask",
"question": "allow",
"read": {
"*": "allow",
"**/.env*": "deny",
@@ -133,46 +135,51 @@
"npm": "@ai-sdk/openai-compatible",
"name": "DuskaDIY",
"options": {
"baseURL": "https://llm.duskadiy.com/api/v1"
"baseURL": "https://llm.duskadiy.com/api/v1",
"apiKey": "{env:DUSKADIY_API_KEY}",
"timeout": 3600000
},
"models": {
"Qwen3-Coder-30B-Instruct-UD-Q3_K_XL": { "name": "Qwen3 Coder 30B · 32k — main agent coder (remote)", "limit": { "context": 32768, "output": 4096 } },
"Qwen3-Coder-Next-UD-IQ3_XXS": { "name": "Qwen3 Coder Next 80B · 128k — long sessions (remote)", "limit": { "context": 131072, "output": 16384 } },
"Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS": { "name": "Qwen3.6 35B · 24k · vision — daily driver (remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.6-35B-A3B-Thinking": { "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.5-9B-UD-Q6_K_XL": { "name": "Qwen3.5 9B · 32k · vision — quick tasks (remote)", "attachment": true, "limit": { "context": 32768, "output": 8192 } },
"Qwen3.8-27B-UD-IQ3_XXS": { "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner (remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"gemma-4-26B-A4B-it-UD-IQ4_XS": { "name": "Gemma 4 26B · 24k · vision — quality generalist (remote)", "attachment": true, "limit": { "context": 24576, "output": 4096 } },
"gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (remote)", "attachment": true, "limit": { "context": 65536, "output": 16384 } },
"GLM-4.7-Flash-UD-Q4_K_XL": { "name": "GLM-4.7 Flash · 24k — quality coder (remote)", "limit": { "context": 24576, "output": 8192 } },
"gpt-oss-20b": { "name": "gpt-oss 20B · 64k — fast reasoning + tools (remote)", "limit": { "context": 65536, "output": 8192 } },
"gpt-oss-20b-low": { "name": "gpt-oss 20B low · 64k — snappy answers (remote)", "limit": { "context": 65536, "output": 8192 } }
"Qwen3-Coder-30B-Instruct-UD-Q3_K_XL": { "name": "Qwen3 Coder 30B · 32k — main agent coder (~30 t/s, remote)", "limit": { "context": 32768, "output": 4096 } },
"Qwen3-Coder-Next-UD-IQ3_XXS": { "name": "Qwen3 Coder Next 80B · 128k — long sessions (~16 t/s, remote)", "limit": { "context": 131072, "output": 16384 } },
"Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS": { "name": "Qwen3.6 35B · 24k · vision — daily driver (~36 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.6-35B-A3B-Thinking": { "reasoning": true, "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (~39 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.5-9B-UD-Q6_K_XL": { "reasoning": true, "name": "Qwen3.5 9B · 32k · vision — quick tasks (~32 t/s, remote)", "attachment": true, "limit": { "context": 32768, "output": 8192 } },
"Qwen3.8-27B-UD-IQ3_XXS": { "reasoning": true, "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner, quiet desktop only (~15 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"gemma-4-26B-A4B-it-UD-IQ4_XS": { "name": "Gemma 4 26B · 24k · vision — quality generalist (~34 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 4096 } },
"gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s, remote)", "attachment": true, "limit": { "context": 65536, "output": 16384 } },
"GLM-4.7-Flash-UD-Q4_K_XL": { "reasoning": true, "interleaved": "reasoning_content", "name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s, remote)", "limit": { "context": 24576, "output": 8192 } },
"gpt-oss-20b": { "reasoning": true, "name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s, remote)", "limit": { "context": 65536, "output": 8192 } },
"gpt-oss-20b-low": { "reasoning": true, "name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s, remote)", "limit": { "context": 65536, "output": 8192 } }
}
},
"llama.cpp": {
"npm": "@ai-sdk/openai-compatible",
"name": "llamacpp",
"options": {
"baseURL": "http://192.168.0.204:11343/v1"
"baseURL": "http://192.168.0.204:11343/v1",
"timeout": 3600000
},
"models": {
"Qwen3-Coder-Next-UD-IQ3_XXS": { "name": "Qwen3 Coder Next 80B · 128k — long sessions", "limit": { "context": 131072, "output": 16384 } },
"Qwen3-Coder-30B-Instruct-UD-Q3_K_XL": { "name": "Qwen3 Coder 30B · 32k — main agent coder", "limit": { "context": 32768, "output": 4096 } },
"Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS": { "name": "Qwen3.6 35B · 24k · vision — daily driver", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.6-35B-A3B-Thinking": { "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.5-9B-UD-Q6_K_XL": { "name": "Qwen3.5 9B · 32k · vision — quick tasks", "attachment": true, "limit": { "context": 32768, "output": 8192 } },
"Qwen3.8-27B-UD-IQ3_XXS": { "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"gemma-4-26B-A4B-it-UD-IQ4_XS": { "name": "Gemma 4 26B · 24k · vision — quality generalist", "attachment": true, "limit": { "context": 24576, "output": 4096 } },
"gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs", "attachment": true, "limit": { "context": 65536, "output": 16384 } },
"GLM-4.7-Flash-UD-Q4_K_XL": { "name": "GLM-4.7 Flash · 24k — quality coder", "limit": { "context": 24576, "output": 8192 } },
"gpt-oss-20b": { "name": "gpt-oss 20B · 64k — fast reasoning + tools", "limit": { "context": 65536, "output": 8192 } },
"gpt-oss-20b-low": { "name": "gpt-oss 20B low · 64k — snappy answers", "limit": { "context": 65536, "output": 8192 } }
"Qwen3-Coder-Next-UD-IQ3_XXS": { "name": "Qwen3 Coder Next 80B · 128k — long sessions (~16 t/s)", "limit": { "context": 131072, "output": 16384 } },
"Qwen3-Coder-30B-Instruct-UD-Q3_K_XL": { "name": "Qwen3 Coder 30B · 32k — main agent coder (~30 t/s)", "limit": { "context": 32768, "output": 4096 } },
"Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS": { "name": "Qwen3.6 35B · 24k · vision — daily driver (~36 t/s)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.6-35B-A3B-Thinking": { "reasoning": true, "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (~39 t/s)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.5-9B-UD-Q6_K_XL": { "reasoning": true, "name": "Qwen3.5 9B · 32k · vision — quick tasks (~32 t/s)", "attachment": true, "limit": { "context": 32768, "output": 8192 } },
"Qwen3.8-27B-UD-IQ3_XXS": { "reasoning": true, "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner, quiet desktop only (~15 t/s)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"gemma-4-26B-A4B-it-UD-IQ4_XS": { "name": "Gemma 4 26B · 24k · vision — quality generalist (~34 t/s)", "attachment": true, "limit": { "context": 24576, "output": 4096 } },
"gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s)", "attachment": true, "limit": { "context": 65536, "output": 16384 } },
"GLM-4.7-Flash-UD-Q4_K_XL": { "reasoning": true, "interleaved": "reasoning_content", "name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s)", "limit": { "context": 24576, "output": 8192 } },
"gpt-oss-20b": { "reasoning": true, "name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s)", "limit": { "context": 65536, "output": 8192 } },
"gpt-oss-20b-low": { "reasoning": true, "name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s)", "limit": { "context": 65536, "output": 8192 } }
}
}
},
"agent": {
"plan": {
"mode": "primary",
"color": "info",
"steps": 30,
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"permission": {
"edit": "deny"
@@ -180,6 +187,8 @@
},
"build": {
"mode": "primary",
"color": "primary",
"steps": 30,
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"permission": {
"edit": {
@@ -197,11 +206,108 @@
}
}
},
"auto": {
"description": "Auto mode: acts without asking; only irreversible or outward-facing actions ask",
"mode": "primary",
"color": "#f9e2af",
"steps": 30,
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"permission": {
"edit": {
"*": "allow",
"**/.env*": "deny",
"**/*.env": "deny",
"**/*.pem": "deny",
"**/*.key": "deny",
"**/id_rsa*": "deny",
"**/.ssh/**": "deny",
"**/.aws/**": "deny",
"**/.git/**": "deny",
"**/.npmrc": "deny",
"**/.netrc": "deny"
},
"webfetch": "allow",
"websearch": "allow",
"external_directory": "ask",
"question": "allow",
"bash": {
"*": "allow",
"git *": "allow",
"git commit*": "ask",
"git push*": "ask",
"git reset*": "ask",
"git checkout*": "ask",
"git switch*": "ask",
"git restore*": "ask",
"git rebase*": "ask",
"git merge*": "ask",
"git cherry-pick*": "ask",
"git stash*": "ask",
"git clean*": "ask",
"git branch -*": "ask",
"git tag*": "ask",
"git remote add*": "ask",
"git remote remove*": "ask",
"git remote set-url*": "ask",
"rm *": "ask",
"rmdir *": "ask",
"kill *": "ask",
"pkill *": "ask",
"killall *": "ask",
"systemctl *": "ask",
"curl *": "ask",
"wget *": "ask",
"ssh *": "ask",
"scp *": "ask",
"rsync *": "ask",
"npm install*": "ask",
"npm i": "ask",
"npm i *": "ask",
"npm ci*": "ask",
"pnpm install*": "ask",
"pnpm i": "ask",
"pnpm i *": "ask",
"pnpm add*": "ask",
"yarn add*": "ask",
"yarn install*": "ask",
"pip install*": "ask",
"pip3 install*": "ask",
"pipx install*": "ask",
"cargo install*": "ask",
"go install*": "ask",
"gem install*": "ask",
"yay *": "ask",
"sudo *": "deny",
"doas *": "deny",
"dd *": "deny",
"mkfs*": "deny",
"shred *": "deny",
"chmod -R *": "deny",
"rm -rf /*": "deny",
"rm -rf ~*": "deny",
"cat .env*": "deny",
"cat *.env": "deny",
"cat *.pem": "deny",
"cat *.key": "deny",
"cat *id_rsa*": "deny",
"cat *secret*": "deny",
"cat *credential*": "deny",
"cat ~/.ssh/*": "deny",
"cat ~/.aws/*": "deny",
"grep .env*": "deny",
"grep * .env*": "deny"
}
}
},
"code-reviewer": {
"description": "Reviews code for best practices and potential issues",
"prompt": "You are a code reviewer. Focus on security, performance, and maintainability.",
"mode": "subagent",
"model": "llama.cpp/GLM-4.7-Flash-UD-Q4_K_XL",
"permission": {
"edit": "deny"
}
@@ -210,7 +316,7 @@
"description": "Runs and interprets test suites",
"prompt": "You are a test runner. Focus on identifying failing tests and providing clear reproduction steps.",
"mode": "subagent",
"model": "llama.cpp/GLM-4.7-Flash-UD-Q4_K_XL",
"hidden": true,
"permission": {
"edit": "deny"
}
@@ -219,7 +325,6 @@
"description": "Maintains project documentation and docstrings",
"prompt": "You are a technical writer. Focus on clarity, accuracy, and keeping documentation in sync with code changes.",
"mode": "subagent",
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"permission": {
"edit": {
"*": "deny",
@@ -235,7 +340,7 @@
"description": "Scans for security vulnerabilities and hardcoded secrets",
"prompt": "You are a security auditor. Focus on OWASP principles, secret detection, and dependency vulnerabilities.",
"mode": "subagent",
"model": "llama.cpp/GLM-4.7-Flash-UD-Q4_K_XL",
"hidden": true,
"permission": {
"edit": "deny"
}
@@ -0,0 +1,262 @@
{
"defs": {
"mocha_rosewater": "#f5e0dc",
"mocha_flamingo": "#f2cdcd",
"mocha_pink": "#f5c2e7",
"mocha_mauve": "#cba6f7",
"mocha_red": "#f38ba8",
"mocha_maroon": "#eba0ac",
"mocha_peach": "#fab387",
"mocha_yellow": "#f9e2af",
"mocha_green": "#a6e3a1",
"mocha_teal": "#94e2d5",
"mocha_sky": "#89dceb",
"mocha_sapphire": "#74c7ec",
"mocha_blue": "#89b4fa",
"mocha_lavender": "#b4befe",
"mocha_text": "#cdd6f4",
"mocha_subtext1": "#bac2de",
"mocha_subtext0": "#a6adc8",
"mocha_overlay2": "#9399b2",
"mocha_overlay1": "#7f849c",
"mocha_overlay0": "#6c7086",
"mocha_surface2": "#585b70",
"mocha_surface1": "#45475a",
"mocha_surface0": "#313244",
"mocha_base": "#1e1e2e",
"mocha_mantle": "#181825",
"mocha_crust": "#11111b",
"mocha_diffAddBg": "#1e2b22",
"mocha_diffDelBg": "#2b1e22",
"latte_rosewater": "#dc8a78",
"latte_flamingo": "#dd7878",
"latte_pink": "#ea76cb",
"latte_mauve": "#8839ef",
"latte_red": "#d20f39",
"latte_maroon": "#e64553",
"latte_peach": "#fe640b",
"latte_yellow": "#df8e1d",
"latte_green": "#40a02b",
"latte_teal": "#179299",
"latte_sky": "#04a5e5",
"latte_sapphire": "#209fb5",
"latte_blue": "#1e66f5",
"latte_lavender": "#7287fd",
"latte_text": "#4c4f69",
"latte_subtext1": "#5c5f77",
"latte_subtext0": "#6c6f85",
"latte_overlay2": "#7c7f93",
"latte_overlay1": "#8c8fa1",
"latte_overlay0": "#9ca0b0",
"latte_surface2": "#acb0be",
"latte_surface1": "#bcc0cc",
"latte_surface0": "#ccd0da",
"latte_base": "#eff1f5",
"latte_mantle": "#e6e9ef",
"latte_crust": "#dce0e8",
"latte_diffAddBg": "#e4f3e6",
"latte_diffDelBg": "#f9e1e6"
},
"theme": {
"primary": {
"dark": "mocha_mauve",
"light": "latte_mauve"
},
"secondary": {
"dark": "mocha_blue",
"light": "latte_blue"
},
"accent": {
"dark": "mocha_peach",
"light": "latte_peach"
},
"error": {
"dark": "mocha_red",
"light": "latte_red"
},
"warning": {
"dark": "mocha_yellow",
"light": "latte_yellow"
},
"success": {
"dark": "mocha_green",
"light": "latte_green"
},
"info": {
"dark": "mocha_sapphire",
"light": "latte_sapphire"
},
"text": {
"dark": "mocha_text",
"light": "latte_text"
},
"textMuted": {
"dark": "mocha_subtext0",
"light": "latte_subtext0"
},
"background": {
"dark": "mocha_base",
"light": "latte_base"
},
"backgroundPanel": {
"dark": "mocha_mantle",
"light": "latte_mantle"
},
"backgroundElement": {
"dark": "mocha_surface0",
"light": "latte_surface0"
},
"border": {
"dark": "mocha_surface1",
"light": "latte_surface1"
},
"borderActive": {
"dark": "mocha_mauve",
"light": "latte_mauve"
},
"borderSubtle": {
"dark": "mocha_surface0",
"light": "latte_surface0"
},
"diffAdded": {
"dark": "mocha_green",
"light": "latte_green"
},
"diffRemoved": {
"dark": "mocha_red",
"light": "latte_red"
},
"diffContext": {
"dark": "mocha_overlay0",
"light": "latte_overlay0"
},
"diffHunkHeader": {
"dark": "mocha_overlay1",
"light": "latte_overlay1"
},
"diffHighlightAdded": {
"dark": "mocha_green",
"light": "latte_green"
},
"diffHighlightRemoved": {
"dark": "mocha_red",
"light": "latte_red"
},
"diffAddedBg": {
"dark": "mocha_diffAddBg",
"light": "latte_diffAddBg"
},
"diffRemovedBg": {
"dark": "mocha_diffDelBg",
"light": "latte_diffDelBg"
},
"diffContextBg": {
"dark": "mocha_base",
"light": "latte_base"
},
"diffLineNumber": {
"dark": "mocha_overlay0",
"light": "latte_overlay0"
},
"diffAddedLineNumberBg": {
"dark": "mocha_diffAddBg",
"light": "latte_diffAddBg"
},
"diffRemovedLineNumberBg": {
"dark": "mocha_diffDelBg",
"light": "latte_diffDelBg"
},
"markdownText": {
"dark": "mocha_text",
"light": "latte_text"
},
"markdownHeading": {
"dark": "mocha_mauve",
"light": "latte_mauve"
},
"markdownLink": {
"dark": "mocha_sapphire",
"light": "latte_sapphire"
},
"markdownLinkText": {
"dark": "mocha_blue",
"light": "latte_blue"
},
"markdownCode": {
"dark": "mocha_green",
"light": "latte_green"
},
"markdownBlockQuote": {
"dark": "mocha_subtext0",
"light": "latte_subtext0"
},
"markdownEmph": {
"dark": "mocha_yellow",
"light": "latte_yellow"
},
"markdownStrong": {
"dark": "mocha_peach",
"light": "latte_peach"
},
"markdownHorizontalRule": {
"dark": "mocha_surface1",
"light": "latte_surface1"
},
"markdownListItem": {
"dark": "mocha_mauve",
"light": "latte_mauve"
},
"markdownListEnumeration": {
"dark": "mocha_mauve",
"light": "latte_mauve"
},
"markdownImage": {
"dark": "mocha_sapphire",
"light": "latte_sapphire"
},
"markdownImageText": {
"dark": "mocha_blue",
"light": "latte_blue"
},
"markdownCodeBlock": {
"dark": "mocha_text",
"light": "latte_text"
},
"syntaxComment": {
"dark": "mocha_overlay0",
"light": "latte_overlay0"
},
"syntaxKeyword": {
"dark": "mocha_mauve",
"light": "latte_mauve"
},
"syntaxFunction": {
"dark": "mocha_blue",
"light": "latte_blue"
},
"syntaxVariable": {
"dark": "mocha_text",
"light": "latte_text"
},
"syntaxString": {
"dark": "mocha_green",
"light": "latte_green"
},
"syntaxNumber": {
"dark": "mocha_peach",
"light": "latte_peach"
},
"syntaxType": {
"dark": "mocha_yellow",
"light": "latte_yellow"
},
"syntaxOperator": {
"dark": "mocha_sky",
"light": "latte_sky"
},
"syntaxPunctuation": {
"dark": "mocha_overlay2",
"light": "latte_overlay2"
}
}
}
@@ -0,0 +1,71 @@
// opencode TUI plugin: one key toggles the whole "verbose view".
//
// The session view keeps four independent display toggles in the TUI's persisted KV store
// (~/.local/state/opencode/kv.json). Built-in commands only flip one each. This plugin reads all
// four and drives them to the same state: if every one is on, turn all off; otherwise turn all on.
// Writing the KV keys directly is what the built-in toggles do too (kv.signal is a reactive
// store), so the open session re-renders immediately and the choice survives restarts.
//
// Declared in tui.json: "plugin": [["./tui-plugins/verbose.ts", { "key": "<leader>v" }]]
// (TUI plugins are never auto-discovered, and this dir is deliberately NOT ~/.config/opencode/plugins/,
// which the server-side plugin loader scans and would reject a TUI-only module.)
// Also available as /verbose and from the command palette.
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
// KV keys + values as used by packages/tui/src/routes/session/index.tsx and context/thinking.ts (v1.18.29)
const KEYS = {
details: "tool_details_visibility", // boolean, default true
generic: "generic_tool_output_visibility", // boolean, default false
timestamps: "timestamps", // "show" | "hide", default "hide"
thinking: "thinking_mode", // "show" | "hide", default "hide"
} as const
const COMMAND = "verbose.toggle"
function current(api: TuiPluginApi) {
return {
details: api.kv.get<boolean>(KEYS.details, true) === true,
generic: api.kv.get<boolean>(KEYS.generic, false) === true,
timestamps: api.kv.get<string>(KEYS.timestamps, "hide") === "show",
thinking: api.kv.get<string>(KEYS.thinking, "hide") === "show",
}
}
function apply(api: TuiPluginApi, on: boolean) {
api.kv.set(KEYS.details, on)
api.kv.set(KEYS.generic, on)
api.kv.set(KEYS.timestamps, on ? "show" : "hide")
api.kv.set(KEYS.thinking, on ? "show" : "hide")
}
const tui: TuiPlugin = async (api, options) => {
const opts = (options ?? {}) as Record<string, unknown>
const key = typeof opts.key === "string" && opts.key.length > 0 ? opts.key : "<leader>v"
api.keymap.registerLayer({
mode: "base",
commands: [
{
name: COMMAND,
title: "Toggle verbose view (tool details, generic output, thinking, timestamps)",
category: "Session",
namespace: "palette",
slashName: "verbose",
run() {
const state = current(api)
const on = !Object.values(state).every(Boolean)
apply(api, on)
api.ui.toast({
variant: "info",
message: on ? "Verbose view on" : "Verbose view off",
duration: 1500,
})
},
},
],
bindings: [{ key, cmd: COMMAND, desc: "Toggle verbose view" }],
})
}
const plugin: TuiPluginModule & { id: string } = { id: "coja.verbose", tui }
export default plugin
+15 -1
View File
@@ -1,4 +1,18 @@
{
"$schema": "https://opencode.ai/tui.json",
"theme": "lucent-orng"
"theme": "lucent-orng",
"plugin": [["./tui-plugins/verbose.ts", { "key": "<leader>v" }]],
"leader_timeout": 1500,
"diff_style": "stacked",
"cursor": { "style": "block", "blinking": false },
"attention": { "enabled": true, "notifications": true, "sound": false },
"keybinds": {
"prompt_stash": "<leader>p",
"prompt_stash_pop": "<leader>o",
"session_fork": "<leader>f",
"messages_last_user": "<leader>k",
"display_thinking": "<leader>i",
"tool_details": "<leader>d",
"session_toggle_timestamps": "<leader>w"
}
}
+16 -1
View File
@@ -130,7 +130,22 @@ bind S {
# status-format[1] is tmux 3.7's default content template (status-left/windows/right).
set -g status 2
set -g status-format[0] "#[fg=#6c7086,bg=default]#(yes ─ | head -n 400 | paste -sd '')"
set -g status-format[1] "#[align=left range=left #{E:status-left-style}]#[push-default]#{T;=/#{status-left-length}:status-left}#[pop-default]#[norange default]#[list=on align=#{status-justify}]#[list=left-marker]<#[list=right-marker]>#[list=on]#{W:#[range=window|#{window_index} #{E:window-status-style}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-format}#[pop-default]#[norange default]#{?loop_last_flag,,#{E:window-status-separator}},#[range=window|#{window_index} list=focus #{?#{!=:#{E:window-status-current-style},default},#{E:window-status-current-style},#{E:window-status-style}}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-current-format}#[pop-default]#[norange list=on default]#{?loop_last_flag,,#{E:window-status-separator}}}#[nolist align=right range=right #{E:status-right-style}]#[push-default]#{T;=/#{status-right-length}:status-right}#[pop-default]#[norange default]"
set -g status-format[1] "#[align=left range=left #{E:status-left-style}]#[push-default]#{T;=/#{status-left-length}:status-left}#[pop-default]#[norange default]#[list=on align=#{status-justify}]#[list=left-marker]<#[list=right-marker]>#[list=on]#{W:#[range=window|#{window_index} #{E:window-status-style}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-format}#[pop-default]#[norange default]#{?loop_last_flag,,#{E:window-status-separator}},#[range=window|#{window_index} list=focus #{?#{!=:#{E:window-status-current-style},default},#{E:window-status-current-style},#{E:window-status-style}}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-current-format}#[pop-default]#[norange list=on default]#{?loop_last_flag,,#{E:window-status-separator}}}#[range=user|newwin] #[fg=#6c7086]+#[default] #[norange]#[nolist align=right range=right #{E:status-right-style}]#[push-default]#{T;=/#{status-right-length}:status-right}#[pop-default]#[norange default]"
# The `+` appended to the window list above sits in a `range=user|newwin`
# marker. Per tmux(1), a user range triggers the same `Status` mouse key as a
# window range and exposes its argument in #{mouse_status_range} — window ranges
# report the literal string "window" — so one binding can serve both: click a
# tab to select it (tmux's stock behaviour, kept in the else branch), click the
# `+` to open a new window. Padded with spaces on both sides so it's a wide
# enough target to hit with a thumb.
bind -n MouseDown1Status {
if -F '#{==:#{mouse_status_range},newwin}' {
new-window
} {
switch-client -t =
}
}
# Round out the theme so no default green leaks into borders/messages/modes.
set -g pane-border-style "fg=#313244"
+10 -4
View File
@@ -63,11 +63,16 @@ have a slash command to disable them for the current session without deleting.
| `permission-gate.ts` | Prompts before dangerous `bash` commands (blocks outright with no UI). Local list covers git history rewrites, package managers, `systemctl` and irreversible disk ops on top of upstream's `rm -rf`/`sudo`/`chmod 777` | **adapted** from pi's `examples/extensions/permission-gate.ts` | delete file |
| `protected-paths.ts` | Blocks `write`/`edit` to secrets and repo plumbing (`secrets.fish`, `auth.json`, `.env`, `.ssh/`, `.git/`, …) | **adapted** from pi's `examples/extensions/protected-paths.ts` | delete file |
| `compaction-watch.ts` | Surfaces every compaction and its `reason`; loudly flags `overflow` (recovery rather than prevention), which means `reserveTokens` is below the model's `maxTokens` | **custom** | delete file |
| `verbose.ts` | `/verbose [on|off|status]` and **Alt+V** toggle the *persisted* verbose default (`hideThinkingBlock`, `showCacheMissNotices`) and report pi's live keys — `ctrl+o` tool output, `ctrl+t` thinking, which an extension cannot drive itself | **custom** | delete file; the live keys are built in and keep working |
| `confirm-destructive.ts` | Confirms before session clear / switch / fork | **verbatim** from pi's `examples/extensions/confirm-destructive.ts` | delete file |
| `session-name.ts` | `/session-name [name]` — friendly names in the session picker instead of the first message | **verbatim** from pi's `examples/extensions/session-name.ts` | delete file |
| `todo.ts` | A `todo` tool the model calls (add/toggle/list/clear) plus `/todos` to view it; state lives in tool results, so forking rewinds it correctly. Distinct from plan-mode's checklist | **verbatim** from pi's `examples/extensions/todo.ts` | delete file (it also adds a tool schema to every request) |
> The bundled examples live at `/opt/pi-coding-agent/examples/extensions/` — that's
> The bundled examples live under pi's install prefix, which moves: it was
> `/opt/pi-coding-agent/` before 0.85 and is `/usr/lib/pi-coding-agent/` on current Arch
> packages. Rather than hardcode either, resolve it —
> `set -l PI_ROOT (dirname (readlink -f (which pi)))` — and read `$PI_ROOT/examples/extensions/`.
> That's
> where `notify`/`questionnaire`/`plan-mode` came from. `plan-mode/` is now the only
> **fork** left, so it's the only one to re-pull after a pi upgrade before re-applying
> the local edits (its header documents them). `notify`/`questionnaire` carry no edits
@@ -103,7 +108,8 @@ pi install npm:pi-vim
>
> ```fish
> cd ~/.pi/agent/npm
> fnm exec --using=22 -- npm install --save-exact @earendil-works/pi-coding-agent@0.83.0
> fnm exec --using=22 -- npm install --save-exact \
> "@earendil-works/pi-coding-agent@"(pi --version | string trim)
> ```
>
> `pi install` deliberately doesn't pull peers (it would duplicate the runtime). Costs
@@ -142,11 +148,11 @@ working diff). Both are **custom**. Format is YAML frontmatter + body with
`keybindings.json` overrides three actions. **A user entry replaces the default keys
for that action — it does not merge.** Action ids and their stock defaults are in
`/opt/pi-coding-agent/docs/keybindings.md`.
`$PI_ROOT/docs/keybindings.md` (see above for `$PI_ROOT`).
```jsonc
{
"app.thinking.cycle": ["ctrl+shift+t"], // moved OFF Shift+Tab so plan-mode can bind it
"app.thinking.cycle": ["alt+t"], // moved OFF Shift+Tab so plan-mode can bind it (was ctrl+shift+t = kitty new_tab, never reached pi)
"tui.editor.cursorUp": ["up", "ctrl+p"], // adds Ctrl+P as up (Emacs-style)
"app.model.cycleForward": ["alt+p"] // Alt+P cycles models
}
@@ -0,0 +1,12 @@
# Upstream example files as last reviewed against pi 0.85.1.
# Written by pi-upgrade-check -a. A file is only re-raised when UPSTREAM moves,
# so our own local edits stop generating noise on every run.
55502f82109df07e922ef3ec41af1b1a5ed33d4d6d56076654ab2bc88e2e0888 confirm-destructive.ts
70e4333e09ce00d546c116fd2e918abf7616c70b5da6e88ba8ee21a326afd483 notify.ts
e32086ba9dd9b4ba0fd308b65241d9affa0b5ecd3f90ef7ea1b9f2b30cab8cc1 permission-gate.ts
9ca66b1f3b1b9a61cc5ddc660f92cca2ea66c4264a1bdb6b00e0da5844399fe2 protected-paths.ts
3f1f719d81152ff013f5d36a97cc3976388d43f2d89bff0f19827d3443d903e6 questionnaire.ts
a0b9fd94e2cc827931150cc409238b473da635ab1a1cddce633cae5937a3ae24 session-name.ts
e46824d00217e25242c186d41837cc84ca81b23f978500323448502a9a424ee2 todo.ts
90fae53e2fada97165f7488520bd54f3cbfe5d2c7e4cfd3bd2aa07ee796e61e8 plan-mode/index.ts
12ea28d57cc55e68e8d52a2f1d16df8b2e7f2749cd694d41453028002a2bdad9 plan-mode/utils.ts
+3 -1
View File
@@ -49,7 +49,9 @@ function notify(title: string, body: string): void {
}
export default function (pi: ExtensionAPI) {
pi.on("agent_end", async () => {
// `agent_end` fires after each low-level run; Pi may still retry, compact,
// or continue with queued follow-ups. Notify only after the full run settles.
pi.on("agent_settled", async () => {
notify("Pi", "Ready for input");
});
}
+114
View File
@@ -0,0 +1,114 @@
/**
* Verbose view toggle `/verbose` and a bound key.
*
* The counterpart of the opencode TUI plugin and the Claude `/verbose` skill, adjusted for
* what pi actually allows. pi splits verbosity in two:
*
* LIVE `ctrl+o` (app.tools.expand) collapses/expands tool output and `ctrl+t`
* (app.thinking.toggle) does the same for thinking blocks. These are built-in
* keybindings. An extension cannot drive them there is no settings API and no
* way to dispatch a built-in action and a key maps to one action, so they
* cannot be merged into a single press the way opencode's four toggles were.
*
* PERSISTED `hideThinkingBlock` and `showCacheMissNotices` in settings.json, read at
* startup. That is what this toggles, i.e. the default you get on every new
* session, which is exactly the position the Claude skill is in.
*
* So: this sets the default and tells you the live keys. "on" shows thinking and the
* cache/compaction/recovery notices; "off" is deliberately quieter than stock pi (it hides
* thinking), matching how opencode's verbose-off state hides thinking too.
*
* pi writes settings.json itself (that is where `lastChangelogVersion` churn comes from),
* so a running pi may overwrite this on exit. If a toggle seems not to have stuck, re-run it
* after pi has exited.
*/
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { getAgentDir } from "@earendil-works/pi-coding-agent";
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
type Settings = Record<string, unknown>;
const LIVE = "live now: ctrl+o tool output, ctrl+t thinking";
// Verbose-on values. Verbose-off is the inverse, not pi's defaults: stock pi already shows
// thinking (hideThinkingBlock defaults to false), so an "off" that merely restored defaults
// would be indistinguishable from "on" for the noisiest part.
const VERBOSE_ON: Settings = { hideThinkingBlock: false, showCacheMissNotices: true };
const VERBOSE_OFF: Settings = { hideThinkingBlock: true, showCacheMissNotices: false };
function settingsPath(): string {
return join(getAgentDir(), "settings.json");
}
function read(): Settings {
// A parse failure must never lead to a write — that would replace a hand-edited config
// with whatever this extension thinks it knows.
const raw = readFileSync(settingsPath(), "utf-8");
return JSON.parse(raw) as Settings;
}
function isOn(s: Settings): boolean {
return s.hideThinkingBlock === false && s.showCacheMissNotices === true;
}
function write(s: Settings): void {
// Plain writeFileSync, deliberately NOT write-temp-then-rename: this path is a stow
// symlink into the dots repo, and renaming over it would replace the link with a real
// file, silently detaching the live config from the repo.
writeFileSync(settingsPath(), `${JSON.stringify(s, null, 2)}\n`, "utf-8");
}
function describe(s: Settings): string {
return `hideThinkingBlock: ${s.hideThinkingBlock ?? false}, showCacheMissNotices: ${s.showCacheMissNotices ?? false}`;
}
function toggle(ctx: ExtensionContext, mode: "on" | "off" | "toggle" | "status"): void {
let current: Settings;
try {
current = read();
} catch (err) {
ctx.ui.notify(`verbose: cannot read settings.json — ${err}`, "error");
return;
}
if (mode === "status") {
ctx.ui.notify(`Verbose ${isOn(current) ? "ON" : "OFF"}${describe(current)}. ${LIVE}`, "info");
return;
}
const on = mode === "toggle" ? !isOn(current) : mode === "on";
const next = { ...current, ...(on ? VERBOSE_ON : VERBOSE_OFF) };
try {
write(next);
} catch (err) {
ctx.ui.notify(`verbose: cannot write settings.json — ${err}`, "error");
return;
}
ctx.ui.notify(
`Verbose ${on ? "ON" : "OFF"} — applies to new sessions (settings are read at startup). ${LIVE}`,
"info",
);
}
export default function (pi: ExtensionAPI) {
pi.registerCommand("verbose", {
description: "Toggle the persisted verbose view (usage: /verbose [on|off|status])",
handler: async (args, ctx) => {
const raw = args.trim().toLowerCase();
if (raw && raw !== "on" && raw !== "off" && raw !== "status") {
ctx.ui.notify("usage: /verbose [on|off|status]", "error");
return;
}
toggle(ctx, (raw || "toggle") as "on" | "off" | "toggle" | "status");
},
});
pi.registerShortcut("alt+v", {
description: "Toggle verbose view",
handler: async (ctx) => toggle(ctx, "toggle"),
});
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"app.thinking.cycle": ["ctrl+shift+t"],
"app.thinking.cycle": ["alt+t"],
"tui.editor.cursorUp": ["up", "ctrl+p"],
"app.model.cycleForward": ["alt+p"]
}
+24 -24
View File
@@ -12,7 +12,7 @@
"models": [
{
"id": "gemma-4-E4B-it-UD-Q8_K_XL",
"name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs",
"name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s)",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 65536,
@@ -21,7 +21,7 @@
},
{
"id": "gemma-4-26B-A4B-it-UD-IQ4_XS",
"name": "Gemma 4 26B · 24k · vision — quality generalist",
"name": "Gemma 4 26B · 24k · vision — quality generalist (~34 t/s)",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 24576,
@@ -30,7 +30,7 @@
},
{
"id": "Qwen3-Coder-30B-Instruct-UD-Q3_K_XL",
"name": "Qwen3 Coder 30B · 32k — main agent coder",
"name": "Qwen3 Coder 30B · 32k — main agent coder (~30 t/s)",
"reasoning": false,
"input": ["text"],
"contextWindow": 32768,
@@ -39,7 +39,7 @@
},
{
"id": "Qwen3-Coder-Next-UD-IQ3_XXS",
"name": "Qwen3 Coder Next 80B · 128k — long sessions",
"name": "Qwen3 Coder Next 80B · 128k — long sessions (~16 t/s)",
"reasoning": false,
"input": ["text"],
"contextWindow": 131072,
@@ -48,7 +48,7 @@
},
{
"id": "Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS",
"name": "Qwen3.6 35B · 24k · vision — daily driver",
"name": "Qwen3.6 35B · 24k · vision — daily driver (~36 t/s)",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 24576,
@@ -57,7 +57,7 @@
},
{
"id": "Qwen3.6-35B-A3B-Thinking",
"name": "Qwen3.6 35B Thinking · 24k · vision — hard problems",
"name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (~39 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"],
@@ -67,7 +67,7 @@
},
{
"id": "Qwen3.5-9B-UD-Q6_K_XL",
"name": "Qwen3.5 9B · 32k · vision — quick tasks",
"name": "Qwen3.5 9B · 32k · vision — quick tasks (~32 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"],
@@ -77,17 +77,17 @@
},
{
"id": "Qwen3.8-27B-UD-IQ3_XXS",
"name": "Qwen3.8 27B · 24k · vision — hybrid reasoner",
"name": "Qwen3.8 27B · 24k · vision — hybrid reasoner, quiet desktop only (~15 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"],
"contextWindow": 24576,
"maxTokens": 8192,
"maxTokens": 4096,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
},
{
"id": "GLM-4.7-Flash-UD-Q4_K_XL",
"name": "GLM-4.7 Flash · 24k — quality coder",
"name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text"],
@@ -97,7 +97,7 @@
},
{
"id": "gpt-oss-20b",
"name": "gpt-oss 20B · 64k — fast reasoning + tools",
"name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s)",
"reasoning": true,
"input": ["text"],
"contextWindow": 65536,
@@ -106,7 +106,7 @@
},
{
"id": "gpt-oss-20b-low",
"name": "gpt-oss 20B low · 64k — snappy answers",
"name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s)",
"reasoning": true,
"input": ["text"],
"contextWindow": 65536,
@@ -127,7 +127,7 @@
"models": [
{
"id": "gemma-4-E4B-it-UD-Q8_K_XL",
"name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs",
"name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s)",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 65536,
@@ -136,7 +136,7 @@
},
{
"id": "gemma-4-26B-A4B-it-UD-IQ4_XS",
"name": "Gemma 4 26B · 24k · vision — quality generalist",
"name": "Gemma 4 26B · 24k · vision — quality generalist (~34 t/s)",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 24576,
@@ -145,7 +145,7 @@
},
{
"id": "Qwen3-Coder-30B-Instruct-UD-Q3_K_XL",
"name": "Qwen3 Coder 30B · 32k — main agent coder",
"name": "Qwen3 Coder 30B · 32k — main agent coder (~30 t/s)",
"reasoning": false,
"input": ["text"],
"contextWindow": 32768,
@@ -154,7 +154,7 @@
},
{
"id": "Qwen3-Coder-Next-UD-IQ3_XXS",
"name": "Qwen3 Coder Next 80B · 128k — long sessions",
"name": "Qwen3 Coder Next 80B · 128k — long sessions (~16 t/s)",
"reasoning": false,
"input": ["text"],
"contextWindow": 131072,
@@ -163,7 +163,7 @@
},
{
"id": "Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS",
"name": "Qwen3.6 35B · 24k · vision — daily driver",
"name": "Qwen3.6 35B · 24k · vision — daily driver (~36 t/s)",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 24576,
@@ -172,7 +172,7 @@
},
{
"id": "Qwen3.6-35B-A3B-Thinking",
"name": "Qwen3.6 35B Thinking · 24k · vision — hard problems",
"name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (~39 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"],
@@ -182,7 +182,7 @@
},
{
"id": "Qwen3.5-9B-UD-Q6_K_XL",
"name": "Qwen3.5 9B · 32k · vision — quick tasks",
"name": "Qwen3.5 9B · 32k · vision — quick tasks (~32 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"],
@@ -192,17 +192,17 @@
},
{
"id": "Qwen3.8-27B-UD-IQ3_XXS",
"name": "Qwen3.8 27B · 24k · vision — hybrid reasoner",
"name": "Qwen3.8 27B · 24k · vision — hybrid reasoner, quiet desktop only (~15 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"],
"contextWindow": 24576,
"maxTokens": 8192,
"maxTokens": 4096,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
},
{
"id": "GLM-4.7-Flash-UD-Q4_K_XL",
"name": "GLM-4.7 Flash · 24k — quality coder",
"name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text"],
@@ -212,7 +212,7 @@
},
{
"id": "gpt-oss-20b",
"name": "gpt-oss 20B · 64k — fast reasoning + tools",
"name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s)",
"reasoning": true,
"input": ["text"],
"contextWindow": 65536,
@@ -221,7 +221,7 @@
},
{
"id": "gpt-oss-20b-low",
"name": "gpt-oss 20B low · 64k — snappy answers",
"name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s)",
"reasoning": true,
"input": ["text"],
"contextWindow": 65536,
+1 -1
View File
@@ -58,7 +58,7 @@
"ex": "mode"
}
},
"lastChangelogVersion": "0.83.0",
"lastChangelogVersion": "0.85.1",
"packages": [
"npm:pi-vim"
]
+1 -1
View File
@@ -42,7 +42,7 @@ bind = $mainMod, F, exec, $yt # launch yt alternative
bind = $mainMod, B, exec, $browser # launch web browser
bind = $mainMod, G, exec, gajim # launch messanger
bind = $mainMod, K, exec, keepassxc # launch password manager
bindd = $mainMod, N, Neovim, exec, $term -e nvim
# bindd = $mainMod, N, Neovim, exec, $term -e nvim
bindd = $mainMod, R, $d pyradio, exec, kitty --session ~/.config/kitty/music.conf
bindd = Ctrl+Shift, Escape, Btop, exec, kitty -e btop
bindd = Ctrl+Alt, Delete, Btop, exec, kitty -e btop
+9
View File
@@ -0,0 +1,9 @@
## gui package — put HyDE's script dir on the *session* PATH: the compositor and
## everything it spawns directly (raw exec binds, terminals) resolve bare hyde
## script names through it. Nothing else adds it — HyDE only self-bootstraps PATH
## inside its own wrappers (hyde-shell/app.sh), so a bare script name execed
## outside that lineage is "command not found" (TROUBLESHOOTING.md symptom 8).
## The systemd-unit side is covered by the twin
## gui/.config/environment.d/10-hyde-path.conf — both are needed.
## No ordering constraint vs hyde.fish (it never touches this dir).
fish_add_path --global ~/.local/lib/hyde
-8
View File
@@ -1,8 +0,0 @@
## gui package — waybar helpers. Lives in conf.d/ (fish sources every conf.d/*.fish),
## NOT in host.fish: each host package ships its own host.fish, which stow deploys last
## with --override — a gui/host.fish would be shadowed on every host that has one.
# Reload the custom waybar (kill + relaunch, detached so it survives this shell).
# Same invocation as userprefs.conf exec-once / the Ctrl+Alt+W bind. Killing waybar
# also stops its cava child (cava-waybar.sh is a child of the bar).
abbr wbar 'killall waybar; setsid -f waybar -c ~/.config/waybar/layouts/custom.jsonc -s ~/.config/waybar/layouts/custom.css'
@@ -18,7 +18,7 @@
## Nothing else from hyde.fish is undone here: its aliases lose to config.fish,
## which fish loads after conf.d, and its `df` wrapper is a function we do not
## otherwise define. Delete this file if you ever decide you want starship.
for f in fish_prompt fish_right_prompt
for f in fish_prompt fish_right_prompt fish_mode_prompt
if test -r $__fish_config_dir/functions/$f.fish
source $__fish_config_dir/functions/$f.fish
end
@@ -0,0 +1,3 @@
function fish_mode_prompt --description "Displays the current vi mode"
fish_default_mode_prompt
end
-3
View File
@@ -117,9 +117,6 @@ bindd = $mainMod Shift, T, $d terminal emulator , exec, alacritty
bindd = Control Alt, T, $d terminal emulator , exec, $TERMINAL
# bindd = Control Shift, Alt, T, $d terminal emulator , exec, tilix # launch tilix terminal emulator - using for ssh
bindd = $mainMod Control, T, $d terminal emulator, exec, tilix # launch tilix terminal emulator
# bindd = $mainMod, R, $d pyradio, exec, $TERMINAL pyradio #radio
bindd = $mainMod, I, $d work, exec, kitty --session ~/.config/kitty/c2.conf
bindd = $mainMod, O, $d work, exec, kitty --session ~/.config/kitty/runners.conf
bindd = $mainMod, R, $d pyradio, exec, kitty --session ~/.config/kitty/music.conf
bindd = $mainMod, E, $d file explorer , exec, $EXPLORER
bindd = $mainMod, C, $d neovim, exec, $TERMINAL nvim #editor
+3 -3
View File
@@ -200,13 +200,13 @@ window#waybar.empty #custom-sr_end {
}
window#waybar.cavabar {
background: transparent;
/* background: transparent; */
background: alpha(@main-bg, 0.1);
}
window#waybar.cavabar #custom-cava {
font-size: 24px;
color: alpha(@main-fg, 0.7);
background: rgba(0, 0, 0, 0.2);
color: alpha(@main-fg, 0.5);
margin: 0px;
padding: 0px;
}
+12 -11
View File
@@ -48,34 +48,35 @@ clients:
name: local
api_base: http://192.168.0.204:11343/v1
models: &lan_models
# Speeds are benched decode t/s (clean-night sweeps) — see fl/.config/llamacpp/README.md roster.
- name: Qwen3-Coder-30B-Instruct-UD-Q3_K_XL
max_input_tokens: 30000 # ctx 32768
max_input_tokens: 30000 # ctx 32768 · ~30 t/s — main agent coder
- name: Qwen3-Coder-Next-UD-IQ3_XXS
max_input_tokens: 128000 # ctx 131072
max_input_tokens: 128000 # ctx 131072 · ~16 t/s — long sessions (128k)
- name: Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS
max_input_tokens: 22000 # ctx 24576
max_input_tokens: 22000 # ctx 24576 · ~36 t/s — daily driver
supports_vision: true
- name: Qwen3.6-35B-A3B-Thinking
max_input_tokens: 22000 # ctx 24576
max_input_tokens: 22000 # ctx 24576 · ~39 t/s — hard problems
supports_vision: true
- name: Qwen3.5-9B-UD-Q6_K_XL
max_input_tokens: 30000 # ctx 32768
max_input_tokens: 30000 # ctx 32768 · ~32 t/s — quick tasks
supports_vision: true
- name: Qwen3.8-27B-UD-IQ3_XXS
max_input_tokens: 22000 # ctx 24576
max_input_tokens: 22000 # ctx 24576 · ~15 t/s — hybrid reasoner, quiet desktop only
supports_vision: true
- name: gemma-4-26B-A4B-it-UD-IQ4_XS
max_input_tokens: 22000 # ctx 24576
max_input_tokens: 22000 # ctx 24576 · ~34 t/s — quality generalist, best vision
supports_vision: true
- name: gemma-4-E4B-it-UD-Q8_K_XL
max_input_tokens: 62000 # ctx 65536
max_input_tokens: 62000 # ctx 65536 · ~57 t/s — fast generalist, long docs
supports_vision: true
- name: GLM-4.7-Flash-UD-Q4_K_XL
max_input_tokens: 22000 # ctx 24576
max_input_tokens: 22000 # ctx 24576 · ~21 t/s — quality coder
- name: gpt-oss-20b
max_input_tokens: 62000 # ctx 65536
max_input_tokens: 62000 # ctx 65536 · ~38 t/s — fast reasoning + tools
- name: gpt-oss-20b-low
max_input_tokens: 62000 # ctx 65536
max_input_tokens: 62000 # ctx 65536 · ~37 t/s — snappy answers
# Remote access via duskadiy.com (reachable from anywhere; requires an API key —
# $DUSKADIY_API_KEY, from the gitignored conf.d/secrets.fish). The LAN endpoint above is keyless.
# Same server, same model ids — reuse the list above via a YAML anchor.
+136 -30
View File
@@ -1,10 +1,11 @@
{
"$schema": "https://opencode.ai/config.json",
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"small_model": "llama.cpp/gemma-4-E4B-it-UD-Q8_K_XL",
"instructions": ["CLAUDE.md", "AGENTS.md"],
"share": "disabled",
"autoupdate": "notify",
"tool_output": { "max_lines": 200, "max_bytes": 16384 },
"snapshot": false,
"disabled_providers": [
"gemini",
"anthropic",
@@ -17,6 +18,7 @@
"webfetch": "ask",
"websearch": "ask",
"external_directory": "ask",
"question": "allow",
"read": {
"*": "allow",
"**/.env*": "deny",
@@ -133,7 +135,8 @@
"npm": "@ai-sdk/openai-compatible",
"name": "llamacpp (lw local)",
"options": {
"baseURL": "http://127.0.0.1:11343/v1"
"baseURL": "http://127.0.0.1:11343/v1",
"timeout": 3600000
},
"models": {
"gemma-4-E2B-it-UD-Q4_K_XL": { "name": "Gemma 4 E2B · 4k — fastest on lw (~6 t/s)", "limit": { "context": 4096, "output": 1024 } },
@@ -150,46 +153,51 @@
"npm": "@ai-sdk/openai-compatible",
"name": "DuskaDIY",
"options": {
"baseURL": "https://llm.duskadiy.com/api/v1"
"baseURL": "https://llm.duskadiy.com/api/v1",
"apiKey": "{env:DUSKADIY_API_KEY}",
"timeout": 3600000
},
"models": {
"Qwen3-Coder-30B-Instruct-UD-Q3_K_XL": { "name": "Qwen3 Coder 30B · 32k — main agent coder (remote)", "limit": { "context": 32768, "output": 4096 } },
"Qwen3-Coder-Next-UD-IQ3_XXS": { "name": "Qwen3 Coder Next 80B · 128k — long sessions (remote)", "limit": { "context": 131072, "output": 16384 } },
"Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS": { "name": "Qwen3.6 35B · 24k · vision — daily driver (remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.6-35B-A3B-Thinking": { "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.5-9B-UD-Q6_K_XL": { "name": "Qwen3.5 9B · 32k · vision — quick tasks (remote)", "attachment": true, "limit": { "context": 32768, "output": 8192 } },
"Qwen3.8-27B-UD-IQ3_XXS": { "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner (remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"gemma-4-26B-A4B-it-UD-IQ4_XS": { "name": "Gemma 4 26B · 24k · vision — quality generalist (remote)", "attachment": true, "limit": { "context": 24576, "output": 4096 } },
"gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (remote)", "attachment": true, "limit": { "context": 65536, "output": 16384 } },
"GLM-4.7-Flash-UD-Q4_K_XL": { "name": "GLM-4.7 Flash · 24k — quality coder (remote)", "limit": { "context": 24576, "output": 8192 } },
"gpt-oss-20b": { "name": "gpt-oss 20B · 64k — fast reasoning + tools (remote)", "limit": { "context": 65536, "output": 8192 } },
"gpt-oss-20b-low": { "name": "gpt-oss 20B low · 64k — snappy answers (remote)", "limit": { "context": 65536, "output": 8192 } }
"Qwen3-Coder-30B-Instruct-UD-Q3_K_XL": { "name": "Qwen3 Coder 30B · 32k — main agent coder (~30 t/s, remote)", "limit": { "context": 32768, "output": 4096 } },
"Qwen3-Coder-Next-UD-IQ3_XXS": { "name": "Qwen3 Coder Next 80B · 128k — long sessions (~16 t/s, remote)", "limit": { "context": 131072, "output": 16384 } },
"Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS": { "name": "Qwen3.6 35B · 24k · vision — daily driver (~36 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.6-35B-A3B-Thinking": { "reasoning": true, "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (~39 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.5-9B-UD-Q6_K_XL": { "reasoning": true, "name": "Qwen3.5 9B · 32k · vision — quick tasks (~32 t/s, remote)", "attachment": true, "limit": { "context": 32768, "output": 8192 } },
"Qwen3.8-27B-UD-IQ3_XXS": { "reasoning": true, "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner, quiet desktop only (~15 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"gemma-4-26B-A4B-it-UD-IQ4_XS": { "name": "Gemma 4 26B · 24k · vision — quality generalist (~34 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 4096 } },
"gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s, remote)", "attachment": true, "limit": { "context": 65536, "output": 16384 } },
"GLM-4.7-Flash-UD-Q4_K_XL": { "reasoning": true, "interleaved": "reasoning_content", "name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s, remote)", "limit": { "context": 24576, "output": 8192 } },
"gpt-oss-20b": { "reasoning": true, "name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s, remote)", "limit": { "context": 65536, "output": 8192 } },
"gpt-oss-20b-low": { "reasoning": true, "name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s, remote)", "limit": { "context": 65536, "output": 8192 } }
}
},
"llama.cpp": {
"npm": "@ai-sdk/openai-compatible",
"name": "llamacpp",
"options": {
"baseURL": "http://192.168.0.204:11343/v1"
"baseURL": "http://192.168.0.204:11343/v1",
"timeout": 3600000
},
"models": {
"Qwen3-Coder-Next-UD-IQ3_XXS": { "name": "Qwen3 Coder Next 80B · 128k — long sessions", "limit": { "context": 131072, "output": 16384 } },
"Qwen3-Coder-30B-Instruct-UD-Q3_K_XL": { "name": "Qwen3 Coder 30B · 32k — main agent coder", "limit": { "context": 32768, "output": 4096 } },
"Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS": { "name": "Qwen3.6 35B · 24k · vision — daily driver", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.6-35B-A3B-Thinking": { "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.5-9B-UD-Q6_K_XL": { "name": "Qwen3.5 9B · 32k · vision — quick tasks", "attachment": true, "limit": { "context": 32768, "output": 8192 } },
"Qwen3.8-27B-UD-IQ3_XXS": { "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"gemma-4-26B-A4B-it-UD-IQ4_XS": { "name": "Gemma 4 26B · 24k · vision — quality generalist", "attachment": true, "limit": { "context": 24576, "output": 4096 } },
"gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs", "attachment": true, "limit": { "context": 65536, "output": 16384 } },
"GLM-4.7-Flash-UD-Q4_K_XL": { "name": "GLM-4.7 Flash · 24k — quality coder", "limit": { "context": 24576, "output": 8192 } },
"gpt-oss-20b": { "name": "gpt-oss 20B · 64k — fast reasoning + tools", "limit": { "context": 65536, "output": 8192 } },
"gpt-oss-20b-low": { "name": "gpt-oss 20B low · 64k — snappy answers", "limit": { "context": 65536, "output": 8192 } }
"Qwen3-Coder-Next-UD-IQ3_XXS": { "name": "Qwen3 Coder Next 80B · 128k — long sessions (~16 t/s)", "limit": { "context": 131072, "output": 16384 } },
"Qwen3-Coder-30B-Instruct-UD-Q3_K_XL": { "name": "Qwen3 Coder 30B · 32k — main agent coder (~30 t/s)", "limit": { "context": 32768, "output": 4096 } },
"Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS": { "name": "Qwen3.6 35B · 24k · vision — daily driver (~36 t/s)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.6-35B-A3B-Thinking": { "reasoning": true, "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (~39 t/s)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.5-9B-UD-Q6_K_XL": { "reasoning": true, "name": "Qwen3.5 9B · 32k · vision — quick tasks (~32 t/s)", "attachment": true, "limit": { "context": 32768, "output": 8192 } },
"Qwen3.8-27B-UD-IQ3_XXS": { "reasoning": true, "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner, quiet desktop only (~15 t/s)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"gemma-4-26B-A4B-it-UD-IQ4_XS": { "name": "Gemma 4 26B · 24k · vision — quality generalist (~34 t/s)", "attachment": true, "limit": { "context": 24576, "output": 4096 } },
"gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s)", "attachment": true, "limit": { "context": 65536, "output": 16384 } },
"GLM-4.7-Flash-UD-Q4_K_XL": { "reasoning": true, "interleaved": "reasoning_content", "name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s)", "limit": { "context": 24576, "output": 8192 } },
"gpt-oss-20b": { "reasoning": true, "name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s)", "limit": { "context": 65536, "output": 8192 } },
"gpt-oss-20b-low": { "reasoning": true, "name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s)", "limit": { "context": 65536, "output": 8192 } }
}
}
},
"agent": {
"plan": {
"mode": "primary",
"color": "info",
"steps": 30,
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"permission": {
"edit": "deny"
@@ -197,6 +205,8 @@
},
"build": {
"mode": "primary",
"color": "primary",
"steps": 30,
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"permission": {
"edit": {
@@ -214,11 +224,108 @@
}
}
},
"auto": {
"description": "Auto mode: acts without asking; only irreversible or outward-facing actions ask",
"mode": "primary",
"color": "#f9e2af",
"steps": 30,
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"permission": {
"edit": {
"*": "allow",
"**/.env*": "deny",
"**/*.env": "deny",
"**/*.pem": "deny",
"**/*.key": "deny",
"**/id_rsa*": "deny",
"**/.ssh/**": "deny",
"**/.aws/**": "deny",
"**/.git/**": "deny",
"**/.npmrc": "deny",
"**/.netrc": "deny"
},
"webfetch": "allow",
"websearch": "allow",
"external_directory": "ask",
"question": "allow",
"bash": {
"*": "allow",
"git *": "allow",
"git commit*": "ask",
"git push*": "ask",
"git reset*": "ask",
"git checkout*": "ask",
"git switch*": "ask",
"git restore*": "ask",
"git rebase*": "ask",
"git merge*": "ask",
"git cherry-pick*": "ask",
"git stash*": "ask",
"git clean*": "ask",
"git branch -*": "ask",
"git tag*": "ask",
"git remote add*": "ask",
"git remote remove*": "ask",
"git remote set-url*": "ask",
"rm *": "ask",
"rmdir *": "ask",
"kill *": "ask",
"pkill *": "ask",
"killall *": "ask",
"systemctl *": "ask",
"curl *": "ask",
"wget *": "ask",
"ssh *": "ask",
"scp *": "ask",
"rsync *": "ask",
"npm install*": "ask",
"npm i": "ask",
"npm i *": "ask",
"npm ci*": "ask",
"pnpm install*": "ask",
"pnpm i": "ask",
"pnpm i *": "ask",
"pnpm add*": "ask",
"yarn add*": "ask",
"yarn install*": "ask",
"pip install*": "ask",
"pip3 install*": "ask",
"pipx install*": "ask",
"cargo install*": "ask",
"go install*": "ask",
"gem install*": "ask",
"yay *": "ask",
"sudo *": "deny",
"doas *": "deny",
"dd *": "deny",
"mkfs*": "deny",
"shred *": "deny",
"chmod -R *": "deny",
"rm -rf /*": "deny",
"rm -rf ~*": "deny",
"cat .env*": "deny",
"cat *.env": "deny",
"cat *.pem": "deny",
"cat *.key": "deny",
"cat *id_rsa*": "deny",
"cat *secret*": "deny",
"cat *credential*": "deny",
"cat ~/.ssh/*": "deny",
"cat ~/.aws/*": "deny",
"grep .env*": "deny",
"grep * .env*": "deny"
}
}
},
"code-reviewer": {
"description": "Reviews code for best practices and potential issues",
"prompt": "You are a code reviewer. Focus on security, performance, and maintainability.",
"mode": "subagent",
"model": "llama.cpp/GLM-4.7-Flash-UD-Q4_K_XL",
"permission": {
"edit": "deny"
}
@@ -227,7 +334,7 @@
"description": "Runs and interprets test suites",
"prompt": "You are a test runner. Focus on identifying failing tests and providing clear reproduction steps.",
"mode": "subagent",
"model": "llama.cpp/GLM-4.7-Flash-UD-Q4_K_XL",
"hidden": true,
"permission": {
"edit": "deny"
}
@@ -236,7 +343,6 @@
"description": "Maintains project documentation and docstrings",
"prompt": "You are a technical writer. Focus on clarity, accuracy, and keeping documentation in sync with code changes.",
"mode": "subagent",
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"permission": {
"edit": {
"*": "deny",
@@ -252,7 +358,7 @@
"description": "Scans for security vulnerabilities and hardcoded secrets",
"prompt": "You are a security auditor. Focus on OWASP principles, secret detection, and dependency vulnerabilities.",
"mode": "subagent",
"model": "llama.cpp/GLM-4.7-Flash-UD-Q4_K_XL",
"hidden": true,
"permission": {
"edit": "deny"
}
+22 -22
View File
@@ -97,7 +97,7 @@
"models": [
{
"id": "gemma-4-E4B-it-UD-Q8_K_XL",
"name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs",
"name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s)",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 65536,
@@ -106,7 +106,7 @@
},
{
"id": "gemma-4-26B-A4B-it-UD-IQ4_XS",
"name": "Gemma 4 26B · 24k · vision — quality generalist",
"name": "Gemma 4 26B · 24k · vision — quality generalist (~34 t/s)",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 24576,
@@ -115,7 +115,7 @@
},
{
"id": "Qwen3-Coder-30B-Instruct-UD-Q3_K_XL",
"name": "Qwen3 Coder 30B · 32k — main agent coder",
"name": "Qwen3 Coder 30B · 32k — main agent coder (~30 t/s)",
"reasoning": false,
"input": ["text"],
"contextWindow": 32768,
@@ -124,7 +124,7 @@
},
{
"id": "Qwen3-Coder-Next-UD-IQ3_XXS",
"name": "Qwen3 Coder Next 80B · 128k — long sessions",
"name": "Qwen3 Coder Next 80B · 128k — long sessions (~16 t/s)",
"reasoning": false,
"input": ["text"],
"contextWindow": 131072,
@@ -133,7 +133,7 @@
},
{
"id": "Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS",
"name": "Qwen3.6 35B · 24k · vision — daily driver",
"name": "Qwen3.6 35B · 24k · vision — daily driver (~36 t/s)",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 24576,
@@ -142,7 +142,7 @@
},
{
"id": "Qwen3.6-35B-A3B-Thinking",
"name": "Qwen3.6 35B Thinking · 24k · vision — hard problems",
"name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (~39 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"],
@@ -152,7 +152,7 @@
},
{
"id": "Qwen3.5-9B-UD-Q6_K_XL",
"name": "Qwen3.5 9B · 32k · vision — quick tasks",
"name": "Qwen3.5 9B · 32k · vision — quick tasks (~32 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"],
@@ -162,7 +162,7 @@
},
{
"id": "Qwen3.8-27B-UD-IQ3_XXS",
"name": "Qwen3.8 27B · 24k · vision — hybrid reasoner",
"name": "Qwen3.8 27B · 24k · vision — hybrid reasoner, quiet desktop only (~15 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"],
@@ -172,7 +172,7 @@
},
{
"id": "GLM-4.7-Flash-UD-Q4_K_XL",
"name": "GLM-4.7 Flash · 24k — quality coder",
"name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text"],
@@ -182,7 +182,7 @@
},
{
"id": "gpt-oss-20b",
"name": "gpt-oss 20B · 64k — fast reasoning + tools",
"name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s)",
"reasoning": true,
"input": ["text"],
"contextWindow": 65536,
@@ -191,7 +191,7 @@
},
{
"id": "gpt-oss-20b-low",
"name": "gpt-oss 20B low · 64k — snappy answers",
"name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s)",
"reasoning": true,
"input": ["text"],
"contextWindow": 65536,
@@ -212,7 +212,7 @@
"models": [
{
"id": "gemma-4-E4B-it-UD-Q8_K_XL",
"name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs",
"name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s)",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 65536,
@@ -221,7 +221,7 @@
},
{
"id": "gemma-4-26B-A4B-it-UD-IQ4_XS",
"name": "Gemma 4 26B · 24k · vision — quality generalist",
"name": "Gemma 4 26B · 24k · vision — quality generalist (~34 t/s)",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 24576,
@@ -230,7 +230,7 @@
},
{
"id": "Qwen3-Coder-30B-Instruct-UD-Q3_K_XL",
"name": "Qwen3 Coder 30B · 32k — main agent coder",
"name": "Qwen3 Coder 30B · 32k — main agent coder (~30 t/s)",
"reasoning": false,
"input": ["text"],
"contextWindow": 32768,
@@ -239,7 +239,7 @@
},
{
"id": "Qwen3-Coder-Next-UD-IQ3_XXS",
"name": "Qwen3 Coder Next 80B · 128k — long sessions",
"name": "Qwen3 Coder Next 80B · 128k — long sessions (~16 t/s)",
"reasoning": false,
"input": ["text"],
"contextWindow": 131072,
@@ -248,7 +248,7 @@
},
{
"id": "Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS",
"name": "Qwen3.6 35B · 24k · vision — daily driver",
"name": "Qwen3.6 35B · 24k · vision — daily driver (~36 t/s)",
"reasoning": false,
"input": ["text", "image"],
"contextWindow": 24576,
@@ -257,7 +257,7 @@
},
{
"id": "Qwen3.6-35B-A3B-Thinking",
"name": "Qwen3.6 35B Thinking · 24k · vision — hard problems",
"name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (~39 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"],
@@ -267,7 +267,7 @@
},
{
"id": "Qwen3.5-9B-UD-Q6_K_XL",
"name": "Qwen3.5 9B · 32k · vision — quick tasks",
"name": "Qwen3.5 9B · 32k · vision — quick tasks (~32 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"],
@@ -277,7 +277,7 @@
},
{
"id": "Qwen3.8-27B-UD-IQ3_XXS",
"name": "Qwen3.8 27B · 24k · vision — hybrid reasoner",
"name": "Qwen3.8 27B · 24k · vision — hybrid reasoner, quiet desktop only (~15 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"],
@@ -287,7 +287,7 @@
},
{
"id": "GLM-4.7-Flash-UD-Q4_K_XL",
"name": "GLM-4.7 Flash · 24k — quality coder",
"name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text"],
@@ -297,7 +297,7 @@
},
{
"id": "gpt-oss-20b",
"name": "gpt-oss 20B · 64k — fast reasoning + tools",
"name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s)",
"reasoning": true,
"input": ["text"],
"contextWindow": 65536,
@@ -306,7 +306,7 @@
},
{
"id": "gpt-oss-20b-low",
"name": "gpt-oss 20B low · 64k — snappy answers",
"name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s)",
"reasoning": true,
"input": ["text"],
"contextWindow": 65536,
+30 -1
View File
@@ -4,6 +4,7 @@
"defaultThinkingLevel": "off",
"theme": "catppuccin-mocha",
"quietStartup": false,
"collapseChangelog": true,
"defaultProjectTrust": "ask",
"enableInstallTelemetry": false,
"enabledModels": [
@@ -33,5 +34,33 @@
"npm"
],
"showHardwareCursor": true,
"lastChangelogVersion": "0.83.0"
"piVim": {
"clipboardMirror": "yank",
"exCommand": {
"piDispatch": true,
"copyInputToClipboard": false
},
"modeColors": {
"insert": "success",
"normal": "accent",
"visual": "warning",
"ex": "bashMode"
},
"borderSync": {
"insert": "host",
"normal": "host",
"visual": "host",
"ex": "host"
},
"labelSync": {
"insert": "mode",
"normal": "mode",
"visual": "mode",
"ex": "mode"
}
},
"lastChangelogVersion": "0.83.0",
"packages": [
"npm:pi-vim"
]
}
+64
View File
@@ -0,0 +1,64 @@
# WM HOST OVERLAY — full-file copy of common/.config/aichat/config.yaml with the default model
# flipped to the duskadiy client: wm is not on the home LAN, so the `local` endpoint
# (192.168.0.204) is unreachable here (kept below for VPN use). Stowed with --override so it
# shadows the common file on wm only. When the common config changes, mirror the change here.
# see https://github.com/sigoden/aichat/blob/main/config.example.yaml
keybindings: vi
editor: nvim
model: duskadiy:Qwen3-Coder-30B-Instruct-UD-Q3_K_XL
# Sessions: persist REPL sessions and keep more history before summarizing.
# The default compress_threshold (4000) summarizes far too early for 24k+ windows.
save_session: true
compress_threshold: 16000
# REPL prompts show live context usage (needs max_input_tokens, set per model below)
left_prompt: '{color.green}{?session {session}{?role /}}{role}{color.cyan}{?rag @{rag}}{color.reset}> '
right_prompt: '{color.purple}{?session {consume_tokens}/{max_input_tokens} }{color.reset}'
# NOTE: temperature/top_p are intentionally unset — the LAN server applies tuned
# per-model sampling via --jinja (e.g. GLM 0.6/0.95); a global value would clobber it.
# max_input_tokens = real ctx-size (from the router) minus output headroom.
clients:
# LAN access (fast; only reachable on the home network)
- type: openai-compatible
name: local
api_base: http://192.168.0.204:11343/v1
models: &lan_models
# Speeds are benched decode t/s (clean-night sweeps) — see fl/.config/llamacpp/README.md roster.
- name: Qwen3-Coder-30B-Instruct-UD-Q3_K_XL
max_input_tokens: 30000 # ctx 32768 · ~30 t/s — main agent coder
- name: Qwen3-Coder-Next-UD-IQ3_XXS
max_input_tokens: 128000 # ctx 131072 · ~16 t/s — long sessions (128k)
- name: Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS
max_input_tokens: 22000 # ctx 24576 · ~36 t/s — daily driver
supports_vision: true
- name: Qwen3.6-35B-A3B-Thinking
max_input_tokens: 22000 # ctx 24576 · ~39 t/s — hard problems
supports_vision: true
- name: Qwen3.5-9B-UD-Q6_K_XL
max_input_tokens: 30000 # ctx 32768 · ~32 t/s — quick tasks
supports_vision: true
- name: Qwen3.8-27B-UD-IQ3_XXS
max_input_tokens: 22000 # ctx 24576 · ~15 t/s — hybrid reasoner, quiet desktop only
supports_vision: true
- name: gemma-4-26B-A4B-it-UD-IQ4_XS
max_input_tokens: 22000 # ctx 24576 · ~34 t/s — quality generalist, best vision
supports_vision: true
- name: gemma-4-E4B-it-UD-Q8_K_XL
max_input_tokens: 62000 # ctx 65536 · ~57 t/s — fast generalist, long docs
supports_vision: true
- name: GLM-4.7-Flash-UD-Q4_K_XL
max_input_tokens: 22000 # ctx 24576 · ~21 t/s — quality coder
- name: gpt-oss-20b
max_input_tokens: 62000 # ctx 65536 · ~38 t/s — fast reasoning + tools
- name: gpt-oss-20b-low
max_input_tokens: 62000 # ctx 65536 · ~37 t/s — snappy answers
# Remote access via duskadiy.com (reachable from anywhere; requires an API key —
# $DUSKADIY_API_KEY, from the gitignored conf.d/secrets.fish). The LAN endpoint above is keyless.
# Same server, same model ids — reuse the list above via a YAML anchor.
- type: openai-compatible
name: duskadiy
api_base: https://llm.duskadiy.com/api/v1
models: *lan_models