--- title: Scripts description: The shell-agnostic executables in ~/.local/bin — usage, behavior, and dependencies for each. --- `dot_local/bin/` holds standalone `sh`, `bash`, and `python3` executables. The `executable_` prefix is a chezmoi attribute that sets the executable bit and is dropped from the target name, so `dot_local/bin/executable_pj` becomes `~/.local/bin/pj`. bash, zsh, and fish all put `~/.local/bin` on `PATH`, so every shell runs the same implementation. This page covers the general-purpose scripts. The desktop scripts in the same directory are documented with their compositor: - `niri-launch`, `niri-launch-ai`, `niri-launch-browser`, `niri-launch-terminal`, `niri-launch-webapp`, `cp-appid`: [niri](/desktop/niri) - `omarchy-*`: [Hyprland](/desktop/hyprland) ## Why some helpers are shell functions A script runs as a child process, and a child cannot change its parent shell's working directory or read the parent's in-memory history. Helpers that need to do either are therefore defined as shell functions, ported three times: `fish/functions/*.fish`, `bash/functions/*.bash`, and `zsh/functions/*.zsh`. | Helper | Why it must be a function | | --- | --- | | `yy` (and fish's `y`) | cds into the directory yazi exited in | | `c` | cds when given an argument | | `cd` | Replaces the `cd` builtin with a zoxide fallback | | `ghg`, `ghn`, `gho` | cd into a repo that was picked or cloned | | `clip-last` | Reads the shell's own history (`$history[1]` in fish, `fc` in bash/zsh) | For `ghg` and `ghn`, the work lives in a script that prints a directory on stdout (and everything else on stderr). The shell function captures that path and cds into it. `ghg`'s script is named `ghg-clone` because an unrelated cargo binary already uses the name `ghg`. `gho` has no script: the function reads the path from `tv ghq-repos` ([television](/tools/television#ghq-repos)). Several scripts below also exist as fish functions of the same name (`ai`, `c2p`, `extract_agents`, `fab`, `gdc`, `git-summary`, `lc-elir`, `mdcp`, `mdp`, `launch_omp_livediff_overlay`, `gh-sonr-org-migrate`). In fish the function runs instead of the script. Both versions behave the same. See [fish functions](/shell/fish#functions). ## Overview | Script | Language | One line | | --- | --- | --- | | [`ai`](#ai) | sh | Pick an AI CLI with gum and launch it | | [`atuin-pick`](#atuin-pick) | python3 | atuin history picker for a kitty or tmux split | | [`c2p`](#c2p) | sh | code2prompt with an fzf-picked template | | [`extract_agents`](#extract_agents) | sh | Copy all `AGENTS.md` files into a directory tree | | [`fab`](#fab) | sh | fzf picker for Fabric patterns | | [`gdc`](#gdc) | sh | Discard all git changes after confirmation | | [`gh-sonr-org-migrate`](#gh-sonr-org-migrate) | bash | Consolidate `*sonr*` GitHub orgs into `sonr-io` | | [`ghg-clone`](#ghg-clone) | sh | `ghq get` and print the clone path | | [`ghn`](#ghn) | sh | Create a private repo, clone it, print the path | | [`git-summary`](#git-summary) | sh | Aggregate a repo's `AGENTS.md` files with code2prompt | | [`gop`](#gop) | sh | Open the current repo's web page | | [`kitty-day-save`](#kitty-day-save) | bash | Save kitty's daily session right before shutdown/reboot | | [`launch_omp_livediff_overlay`](#launch_omp_livediff_overlay) | sh | omp overlay plus a livediff split in kitty | | [`lc-elir`](#lc-elir) | sh | Run `/leetcode-eli5` via omp on picked solutions | | [`mandible-pick`](#mandible-pick) | bash | fzf-pick a command, then open it in mandible | | [`mdcp`](#mdcp) | sh | Fetch a URL as Markdown, copy it, preview it | | [`mdp`](#mdp) | sh | fzf Markdown picker with mdfried preview | | [`mtime-sort`](#mtime-sort) | sh + perl | Sort path lines newest-first by mtime | | [`omp-session`](#omp-session) | python3 | Resume or start omp for the current repo; backend of the `omp-sessions` cable | | [`pj`](#pj) | bash | Task runner across every project task source, on a tv cable | | [`pj-palette`](#pj-palette) | bash | pj's tasks plus quick actions as a tmux-palette source | | [`yazi-nvim-open`](#yazi-nvim-open) | sh | yazi opener that targets the host Neovim | ## ai ```sh ai ``` Shows `gum choose --header 'AI CLI'` with `claude`, `opencode`, `codex`, `hermes`, and `fractal`, then `exec`s the choice. `claude` runs as `claude --dangerously-skip-permissions`; the others run as the bare binary name. Esc or Ctrl-C exits 0. **Needs:** `gum` and the chosen CLI. ## atuin-pick ```sh atuin-pick ``` Opens atuin's interactive search (`atuin search -i --filter-mode directory`), limited to history from the current directory. It then `exec`s an interactive `fish`. If you picked a command, the new fish runs `echo '❯ '; ` via `-C` (`--init-command`), so aliases resolve, a `cd` persists, and the prompt appears below the output. Esc starts a plain fish. It is written for launch contexts where atuin's shell init never ran. It runs `atuin` from `PATH` and seeds `ATUIN_SESSION` from `atuin uuid` if unset. **Launched by:** ctrl+shift+p in kitty. `kitty.conf` opens it in a horizontal split (`launch_hs atuin-pick`). Under the kitty-tmux profile, `tmux.conf` runs it with `tmux split-window -v -l 25%` in the current pane's directory. See [kitty](/terminal/kitty) and [atuin](/shell/atuin). **Needs:** `python3`, `atuin`, `fish`. ## c2p ```sh c2p ``` Lists `*.hbs` files under `~/.config/code2prompt` with `fd`, lets you pick one with fzf, and `exec`s `code2prompt --template `. **Needs:** `fd`, `fzf`, `code2prompt`. ## extract_agents ```sh extract_agents ``` Creates `` and copies every `AGENTS.md` below the current directory into it with `cp --parents`, keeping the relative paths. **Needs:** `fd`, GNU `cp` (`--parents`). ## fab ```sh fab some-command | fab ``` Reads `~/.config/fabric/pattern_explainations.json`. fzf lists pattern names, the preview pane (right, 60%, wrapped) shows each description, and ctrl-/ toggles the preview. The picked pattern runs as `fabric-ai --pattern `, falling back to `fabric` if `fabric-ai` is not installed. Piped stdin is buffered to a temp file and fed to the pattern; the temp file is removed on exit. **Needs:** `jq`, `fzf`, `fabric-ai` or `fabric`, and the pattern JSON. ## gdc ```sh gdc ``` Asks `gum confirm "Discard all uncommitted changes? This is irreversible."`. On yes, runs `git reset --hard HEAD` and `git clean -fd`. **Needs:** `gum`, `git`. ## gh-sonr-org-migrate ```sh gh-sonr-org-migrate ``` A bash port of `fish/functions/gh-sonr-org-migrate.fish`, with the same flow: Checks that `gh`, `gum`, and `jq` exist and that `gh auth status` succeeds. Lists your GitHub orgs whose login contains `sonr`, excluding the target `sonr-io`. You pick which to migrate with `gum choose --no-limit`. For each repo in each org, runs `gh api -X POST repos///transfer -f new_owner=sonr-io`. If the name already exists in `sonr-io`, `gum input` asks for a new name (default `-`). An empty name skips the repo. Waits 30 seconds (`gum spin`), because GitHub transfers are asynchronous. For every selected org that had no failed transfers and now has zero repos, asks `gum confirm` and then `gh api -X DELETE orgs/`. **Needs:** `gh` authenticated as owner of every org involved, `gum`, `jq`. :::warning In fish, run this script by its full path (`~/.local/bin/gh-sonr-org-migrate`). The fish file of the same name is a script body with no function in it, so typing just the name runs the fish flow and then this script. See [fish](/shell/fish#functions). ::: ## ghg-clone ```sh ghg-clone ``` Runs `ghq get `, with its output sent to stderr. It then works out the on-disk path: it strips the scheme, any `user@`, the scp-style colon, and a trailing `.git`, and prefixes `github.com/` when the input has no host. It checks that `$(ghq root)///` exists, runs `zoxide add` on it if zoxide is installed, and prints the path. Accepts `https://…`, `ssh://…`, `git@host:user/repo`, `host/user/repo`, or `user/repo`. **Used by:** the `ghg` shell function, which cds into the path, clears the screen, and runs `onefetch`. The fish function `gtm` also uses it to clone migrated repos. **Needs:** `ghq` (`GHQ_ROOT=~/Code`, exported by fish, zsh, and bash), optional `zoxide`. ## ghn ```sh ghn ``` Creates a new **private** repo and clones it: - **prad.codes** (Gitea): pick an org from `tea organizations list`, then `tea repos create --private` and `ghq get https://prad.codes//`. - **github.com**: pick the owner `prdlk` or `wltbs`, then `gh repo create / --private` and `ghq get`. You pick the host with `gum choose`. The script prints the clone directory, and the `ghn` shell function cds into it. **Needs:** `gum`, `ghq`, and `tea` or `gh`. ## git-summary ```sh git-summary [code2prompt flags…] ``` From anywhere inside a git repo, runs `code2prompt --include AGENTS.md --include "**/AGENTS.md" --template ~/.config/code2prompt/agents-md.hbs`. Extra flags such as `--output-file` or `--tokens` are passed through. **Needs:** `git`, `code2prompt`, and the `agents-md.hbs` template. ## gop ```sh gop # origin, else upstream, else first remote gop upstream # a named remote gop owner/repo # github.com/owner/repo gop # any https/ssh/scp remote URL ``` Opens a repo's web page for any forge whose URL layout is `https:////`, such as github.com and prad.codes. It reduces any remote form to `//`: it drops credentials, drops the port on `ssh://` and `git://` remotes, keeps the port and plain `http` on `http://` remotes, and strips `.git`. It then runs `$BROWSER` (default `xdg-open`) on the result. **Needs:** `git`, a browser or `xdg-open`. ## kitty-day-save ```sh kitty-day-save # hold a logind delay lock, flush on shutdown, exit kitty-day-save --now # flush every running kitty immediately ``` Run by `kitty-day-save.service` (`dot_config/systemd/user/`, `WantedBy=graphical-session.target`, `Restart=always` after 10 s). It wraps itself in `systemd-inhibit --what=shutdown --mode=delay` and waits on `gdbus monitor` for login1's `PrepareForShutdown (true,)`. logind sends that before anything is killed and waits for the lock (`InhibitDelayMaxSec`, 5 s by default), so every kitty still has all its windows. It then sends `kitten day_session.py flush` over remote control to each `@kitty-` socket in `/proc/net/unix` (3 s timeout each) and exits, which releases the lock. The kitty that owns today's session saves it and holds further saves for 60 s; other kitties ignore the flush. See [kitty daily sessions](/terminal/kitty#daily-sessions). **Needs:** `systemd-inhibit`, `gdbus`, `kitten`. Linux desktop/laptop roles only (`.chezmoiignore.tmpl`). ## launch_omp_livediff_overlay ```sh launch_omp_livediff_overlay ``` Inside kitty with remote control enabled, runs `kitty @ launch --type=overlay-main --cwd=current omp`, then opens `livediff` in a `vsplit` next to that window with `--keep-focus`. It relies on `$KITTY_LISTEN_ON` rather than a hard-coded socket, because kitty appends `-PID` to `listen_on` socket names. It exits with an error if the variable is unset. **Needs:** kitty with `allow_remote_control`/`listen_on` ([kitty](/terminal/kitty)), `omp` ([AI agents](/ai)), `livediff`. ## lc-elir ```sh lc-elir [root] ``` Finds numbered LeetCode solution files (`^\d+\..+\.(js|ts)$`, for example `1365.some-slug.js`) under `root` (default `.`). fzf multi-selects them, showing the last three path segments with a `bat` preview of the first 40 lines. Each pick runs `omp -p "/leetcode-eli5 "`. **Needs:** `fd`, `fzf`, `bat`, `omp`, and the `/leetcode-eli5` omp command. ## mandible-pick ```sh mandible-pick ``` `mandible` (installed with cargo, `~/.cargo/bin`) needs a tool to open, so this asks for one first. fzf lists the executables installed in the `PATH` directories, without shell builtins, keywords, or aliases. enter opens the highlighted command. If the query contains a space (`git remote add`) or matches nothing, enter uses it exactly as typed, so mandible opens at that subcommand path. Then it `exec`s `mandible [subcommand…]`. esc exits quietly. The command list is built with a bash loop rather than GNU `find -printf`, so it also works on macOS. **Used by:** the tmux-palette **mandible** item (alias `man`), which runs it in a popup. See [tmux](/terminal/tmux#command-palette). **Needs:** `mandible`, `fzf` 0.51+ (`--with-shell`, `transform`, `accept-or-print-query`). ## mdcp ```sh mdcp ``` Fetches the URL as Markdown with `curl.md`, copies it with `wl-copy` or `pbcopy`, then pastes it back into `mdfried` for a rendered preview. **Needs:** `curl.md`, `mdfried`, and `wl-clipboard` or `pbcopy`. ## mdp ```sh mdp ``` Lists `*.md` files up to 3 directories deep with `fd`. fzf shows an `mdfried` preview (right, 60%, wrapped), and the pick opens in `mdfried`. **Needs:** `fd`, `fzf`, `mdfried`. ## mtime-sort ```sh … | mtime-sort [--repo] ``` Reads lines on stdin and prints them newest-first by the mtime of each line's last tab-separated field (the whole line when it has no tab). With `--repo`, it stats `/.git/index` first, so a repo sorts by its last git activity. Lines whose path does not exist are dropped. Ties keep input order. **Used by:** every custom [television](/tools/television) cable except `agents` and `omp-sessions`, and `pj --list`. tv layers match quality and frecency on top of this order. **Needs:** `perl`. ## omp-session ```sh omp-session ``` Starts or resumes omp for the current git repo by fixed rules, so ctrl-a never asks when the answer is obvious. | HEAD | Does | | --- | --- | | Default branch | Resumes the newest session on it when that session was written less than 3 hours ago. Otherwise opens the `omp-sessions` [television](/tools/television#omp-sessions) cable over all of the repo's sessions; enter resumes the pick, esc returns to the prompt. With no sessions at all it starts a new one | | Any other branch | Resumes the newest session on that branch, or starts a new one | | Detached | Opens the `omp-sessions` cable (a new session when there are none) | | Outside a git repo | Plain `omp` | Resuming runs `omp --resume `; omp switches to the session's own cwd. `omp-session --new` runs plain `omp`; it is the cable's ctrl-n action and marks `OMP_SESSION_STATE` like a resume does, so the prompt is not moved after a new session. **Sessions.** Every `*/*.jsonl` under `$PI_CODING_AGENT_DIR/sessions` (default `~/.omp/agent/sessions`) whose `session` header `cwd` is the worktree root or below it, excluding worktrees nested inside it. Session age is the file's mtime (the last write). **Branches.** omp does not record the git branch. A session's branch is the one HEAD was on when the file was last written, replayed from the worktree's `git reflog` (`checkout: moving from A to B`, `Branch: renamed`, `rebase (finish): returning to`). Sessions older than the oldest reflog move belong to the branch that move left; with no moves they all belong to the current branch. The default branch is `origin/HEAD`, else `init.defaultBranch`, `main`, or `master`, whichever exists. **Entry points.** The cable calls back into the script: | Command | Does | | --- | --- | | `omp-session --list` | Prints ` [ · <subdir>]<TAB><session file>` per session, newest first. The title is omp's session title, else the first user message | | `omp-session --preview FILE` | Title, last-write time, cwd, then the text of every user and assistant message (thinking, tool calls, and tool results skipped) | | `omp-session --resume FILE` | Writes `ran` to `$OMP_SESSION_STATE` when set, then `exec omp --resume FILE` (enter) | **Prompt redraw.** The launcher runs `tv omp-sessions --inline` with `OMP_SESSION_STATE` set to a temp file. When the picker is cancelled (no `ran`), it prints `\033[A` to `/dev/tty` so the shell redraws the prompt in place; the key bindings just repaint. **Launched by:** <kbd>ctrl-a</kbd> in fish and zsh. <kbd>ctrl-n</kbd> at the prompt runs `omp` directly, not this script. **Needs:** `python3`, `git`, `omp`, `tv`. ## pj ```sh pj [extra args…] ``` A task runner that searches every task source in the project and lets you pick one in the `pj` [television](/tools/television#pj) cable. **Sources.** Each is listed only when its runner is installed: | Source | Found in | Runner | | --- | --- | --- | | npm scripts | `package.json` | package manager from `packageManager`, else from the lockfile (`bun.lock[b]`, `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`, searched upward), else `npm`, then `<pm> run` | | Make | `GNUmakefile`, `makefile`, `Makefile` | `make`. Targets come from a text parse of rules and `.PHONY`; `make -p` is avoided because it would run `$(shell …)` | | just | `justfile`, `.justfile`, `Justfile`, `JUSTFILE` | `just` | | mise | `mise.toml`, `.mise.toml`, `mise/config.toml`, `.mise/config.toml`, `.config/mise/config.toml`, or a `mise-tasks`/`.mise/tasks`/`mise/tasks` dir | `mise run`. Hidden tasks are skipped, and tasks inherited from parent directories are filtered out | | devbox | `devbox.json` `shell.scripts` | `devbox run` | | poe | `pyproject.toml` `[tool.poe.tasks]` | `poe`, else `uv run poe` | | pdm | `[tool.pdm.scripts]` (skips `_*` and `post_*`) | `pdm run` | | rye | `[tool.rye.scripts]` | `rye run` | | taskipy | `[tool.taskipy.tasks]` | `task`, else `uv run task` | | hatch | `[tool.hatch.envs.<env>.scripts]` as `env:name` | `hatch run` | | console scripts | `[tool.poetry.scripts]`, `[project.scripts]` | `poetry run` for Poetry projects, else `uv run` | **Scanning.** Walks from `$PWD` up to the git toplevel, inclusive. Outside a repo it stops at the first directory that produced tasks. **Picker.** `pj` writes the records to a temp file and runs `tv pj --inline --source-command "cat <tmp>" --input-header <project root>`. Rows are tagged with the tool (`npm`, `pnpm`, `bun`, `make`, `just`, `mise`, `poe`, `py`, …), so typing a tool name filters to it. A directory column appears when more than one project contributed tasks. Tasks are sorted by their manifest's mtime, newest first, through `mtime-sort`. The preview shows the task body: the npm script string, the Make recipe, `just --show`, `mise tasks info`, the devbox script, or the pyproject entry. **Entry points.** The cable calls back into the script: | Command | Does | | --- | --- | | `pj --list` | Prints tab-separated records: display, kind, target, root, runner, manifest | | `pj --preview KIND TARGET ROOT FILE` | Prints the task body for the preview pane | | `pj --run KIND TARGET ROOT RUNNER` | Runs the task (enter) | **Run.** cds into the task's directory and `exec`s the runner. Extra arguments given to `pj` travel to the task through `$PJ_EXTRA` (shell-quoted), with `--` inserted for `npm run`, `mise`, and `devbox`. Tasks named `dev`, `start`, `serve`, `watch`, or namespaced variants like `dev:web` are wrapped in `openlogs` (found on `PATH`, via `mise which`, or in mise's node installs) for a live log tail. Without openlogs they run unwrapped, with a note. **Env.** `PJ_QUIET=1` suppresses the "no task sources found" error (exit status stays 1). The <kbd>down</kbd> bindings use it. `PJ_STATE=FILE` makes `pj` write `open` to `FILE` before launching the picker, and `pj --run` writes `ran`; the file stays empty when there are no task sources. The key bindings read it to tell no tasks, a cancelled picker, and a task that printed output apart and redraw the prompt to match. `pj` itself does no cursor handling, so typing `pj` behaves like any command. **Launched by:** <kbd>ctrl-p</kbd>, and <kbd>down</kbd> on an empty prompt, in fish, zsh, and bash. **Needs:** `bash`, `tv`, `mtime-sort`, `jq`, `python3` 3.11+ (for `tomllib`), plus whichever runners the project uses. ## pj-palette ```sh pj-palette pj-palette --run KIND TARGET ROOT RUNNER pj-palette --cmd ROOT CMD… ``` The source of the **Project Tasks** [tmux command palette](/terminal/tmux#command-palette) (`palettes/tasks.json`). Open it with tmux `prefix t`, Ghostty <kbd>ctrl+shift+space</kbd>, or the "Project Tasks" item in the main palette. With no arguments it reads the active pane's directory (`tmux display -p '#{pane_current_path}'`) and prints palette items as JSON, grouped by category: | Group | Items | Shown when | | --- | --- | --- | | Quick | `<pm> install` (alias `install`, `i`), `<pm> run dev` (`dev`) | `package.json` at the project root. The package manager comes from `packageManager`, then the lockfile, as in `pj` | | Quick | `uv sync` (`sync`), `uv run python` (`py`, `repl`) | `pyproject.toml` and `uv` installed | | Quick | `make` (default target; `m`, `build`) | A Makefile and `make` | | Quick | `go run .`, `cargo run` | `go.mod` / `Cargo.toml` and the toolchain | | Tasks: `<tool>` | Every record from `pj --list`: npm/bun/pnpm/yarn scripts, Make, just, mise, devbox, pyproject task tables and console scripts | Whatever `pj` finds walking up to the git root | The project root is the git toplevel, else the pane's directory. **Run.** Every item opens a full-width split along the bottom of the window (`split-window -v -f -l 25%`), 25% of its height, in the task's directory. The command is passed to `split-window` as separate arguments, so no shell reparses it. `--run` calls `pj --run`, which keeps pj's `openlogs` wrapping of `dev`/`start`/`serve`/`watch` tasks, and `--cmd` runs a quick action. While the command runs, <kbd>ctrl-c</kbd> stops only the command (the script traps `SIGINT`), so the output stays. Once the command exits, the script prints `[exit N] enter or ctrl-c to close`, and <kbd>enter</kbd> or <kbd>ctrl-c</kbd> closes the pane; other keys are ignored. **Needs:** `pj` and its dependencies, `jq`, tmux. ## yazi-nvim-open ```sh yazi-nvim-open [vsplit|split|tabedit] <file>… ``` yazi's `edit` opener. When run inside a Neovim terminal (`$NVIM` set), each file's absolute path goes to the **host** Neovim through `nvim --server "$NVIM" --remote-expr 'v:lua.EditFromYazi(mode, path)'` (defined in the nvim config's `yazi_session.lua`). Outside Neovim it `exec`s a normal `nvim`, and the split modes do nothing. Set `YAZI_OPEN_TRACE=<file>` to log each call. **Used by:** `yazi.toml` (the `edit` opener) and `keymap.toml` (<kbd>ctrl-v</kbd> vsplit, <kbd>ctrl-x</kbd> split). See [yazi](/tools/yazi). **Needs:** `nvim`, `realpath`.