[Sync] llm stuff
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` 0–1, `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%` | `50–80%` | `≥ 80%` (⚠compact) |
|
||||
| 5h usage | `< 50%` | `50–80%` | `≥ 80%` |
|
||||
| Prompt cache (inverted) | `≥ 80%` | `50–80%` | `< 50%` |
|
||||
| Disk | `< 75%` | `75–90%` | `≥ 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 |
|
||||
|
||||
Executable
+32
@@ -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
|
||||
@@ -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
@@ -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; }
|
||||
Executable
+208
@@ -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
@@ -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()
|
||||
Executable
+51
@@ -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()
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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()
|
||||
@@ -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:
|
||||
return ""
|
||||
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:
|
||||
return ""
|
||||
total = usage.get("input_tokens", 0) + usage.get("cache_creation_input_tokens", 0) + read
|
||||
if total > 0:
|
||||
pct = read / total * 100
|
||||
return "♻️ " + color(f"{pct:.0f}%", bucket(pct, 50, 80, invert=True))
|
||||
if pct is None:
|
||||
return ""
|
||||
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),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+256
@@ -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()
|
||||
Executable
+36
@@ -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
|
||||
@@ -30,6 +30,7 @@ nvim
|
||||
vim
|
||||
tetrigo
|
||||
pyradio
|
||||
cliamp
|
||||
px7-radio
|
||||
kew
|
||||
cmatrix
|
||||
|
||||
@@ -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
|
||||
@@ -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)",
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,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"]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"ex": "mode"
|
||||
}
|
||||
},
|
||||
"lastChangelogVersion": "0.83.0",
|
||||
"lastChangelogVersion": "0.85.1",
|
||||
"packages": [
|
||||
"npm:pi-vim"
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user