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:
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 |
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 |
| [`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 '❯ <cmd>'; <cmd>` 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:** <kbd>ctrl+shift+p</kbd> 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 <file>`.
**Needs:** `fd`, `fzf`, `code2prompt`.
## extract_agents
```sh
extract_agents <output-dir>
```
Creates `<output-dir>` 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 <kbd>ctrl-/</kbd> toggles the preview. The picked pattern runs as `fabric-ai --pattern <name>`, 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:
<Steps>
<Step title="Preflight">Checks that `gh`, `gum`, and `jq` exist and that `gh auth status` succeeds.</Step>
<Step title="Discover">Lists your GitHub orgs whose login contains `sonr`, excluding the target `sonr-io`. You pick which to migrate with `gum choose --no-limit`.</Step>
<Step title="Transfer">For each repo in each org, runs `gh api -X POST repos/<org>/<repo>/transfer -f new_owner=sonr-io`. If the name already exists in `sonr-io`, `gum input` asks for a new name (default `<org>-<repo>`). An empty name skips the repo.</Step>
<Step title="Settle">Waits 30 seconds (`gum spin`), because GitHub transfers are asynchronous.</Step>
<Step title="Delete">For every selected org that had no failed transfers and now has zero repos, asks `gum confirm` and then `gh api -X DELETE orgs/<org>`.</Step>
</Steps>
**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 <repo>
```
Runs `ghq get <repo>`, 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)/<host>/<user>/<repo>` 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.
- **prad.codes** (Gitea): pick an org from `tea organizations list`, then `tea repos create --private` and `ghq get https://prad.codes/<org>/<name>`.
- **github.com**: pick the owner `prdlk` or `wltbs`, then `gh repo create <owner>/<name> --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 <repo-root> --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 <url> # any https/ssh/scp remote URL
```
Opens a repo's web page for any forge whose URL layout is `https://<host>/<owner>/<repo>`, such as github.com and prad.codes. It reduces any remote form to `<host>/<owner>/<repo>`: 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`.
## 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 <file>"`.
**Needs:** `fd`, `fzf`, `bat`, `omp`, and the `/leetcode-eli5` omp command.
## mdcp
```sh
mdcp <url>
```
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`.
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 `<path>/.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.
Starts or resumes [omp](/ai/omp) for the current git repo by fixed rules, so <kbd>ctrl-a</kbd> 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; <kbd>enter</kbd> resumes the pick, <kbd>esc</kbd> 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 <session file>`; omp switches to the session's own cwd.
**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 `<age> <branch> <title>[ · <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.
**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 |
**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.
**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.
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).