SLOPSHOPPER

xai-search

xai_search tool: search live X posts through Pi's guarded xAI subscription runner

newguardtoolprocess
A shopper browsing a rack in a slop shop
README

dotfiles

My macOS development setup, managed with GNU Stow. Each folder under stow/ is a package that mirrors $HOME, so stow/nvim/.config/nvim/init.lua becomes ~/.config/nvim/init.lua.

Test Dotfiles

The stack

LayerChoiceNotes
Terminal + multiplexerRex (beta)Default. Ghostty is the fallback terminal; Herdr still runs on agentbox for phone access over Termius.
Editor / IDENeovimlazy.nvim + Lua plugins in stow/nvim/.config/nvim/. Zed and Cursor are retired and archived under old/.
ShellHomebrew Bash 5 + StarshipFZF, zoxide, and mise on top. Fish/Zsh configs under old/ are rollback-only.
Primary agent harnessClaude CodeSettings, hooks, status line, and local mods in stow/claude/.claude/.
Every other modelPiOpenAI, xAI, and the rest run through Pi (stow/pi/.pi/agent/). It can also reach Claude models via pi-claude-bridge.
Reviewer / second opinioncouncil skillOpus 5.5, GPT-6.1 Sol, and Muse Wonder Full review in parallel; in Claude Code, Sol and Muse run through pi -p. Codex stays configured but is not a daily harness.

Everything uses Tokyo Night with MonoLisaCode 14 pt where the app allows it. Rex is the exception: its theme and font are app settings, not files (see Rex app settings).

Vocabulary like tracked source, live configuration, and apply is defined in CONTEXT.md. Agents working in this repo should start with AGENTS.md.

Install on a new Mac

Clone to ~/code/personal/dotfiles; some Claude Code mod paths assume that location.

git clone git@github.com:manifoldfrs/dotfiles.git ~/code/personal/dotfiles
cd ~/code/personal/dotfiles
./scripts/bootstrap.sh

Bootstrap installs Homebrew and the Brewfile, installs Claude Code with its native installer, backs up conflicting live files (.backup.YYYYMMDDhhmmss), applies all Stow packages, syncs Herdr plugins, selects Homebrew Bash as the login shell, restores Neovim plugins from lazy-lock.json, installs the global npm packages, sets up the global anti-slop runtime and Plannotator, and runs rex skill setup when Rex is present.

Rex itself installs from its DMG and updates itself, so install it separately. Then quit and reopen your terminal (or exec /opt/homebrew/bin/bash --login) and sign in to each harness (claude, pi with /login, codex).

Machine-local secrets go in ~/.config/bash/local.bash, which is never tracked:

export REF_API_KEY="..."
export EXA_API_KEY="..."
export TYPESAFE_API_KEY="..."

Update an existing machine

Day to day, this is all you need:

cd ~/code/personal/dotfiles
git pull
./scripts/stow.sh dry-run   # preview; this is also the default with no argument
./scripts/stow.sh apply     # validates in isolation first, then restows into $HOME

apply runs scripts/validate-dotfiles.sh against a temporary home before touching the live one. If validation fails, nothing changes. Use ./scripts/stow.sh delete to remove all managed symlinks, and ./scripts/bootstrap.sh only when you also want packages, plugins, and tooling refreshed.

The default profile is the STOW_PACKAGES list in scripts/stow.sh: bash git ghostty herdr rex nvim bin claude codex pi agents. To restow a single package by hand:

stow --no-folding -R -v -t "$HOME" -d stow nvim

Edit either the $HOME path or the repo path; symlinks make them the same file. To capture live config back into the repo, run ./scripts/backup.sh and review the diff before committing. It deliberately skips agent account and runtime state.

Migrating a machine with existing config files

Stow will not overwrite real files. Check whether a target is already a Stow symlink with readlink ~/.config/nvim (and friends). If it prints nothing, move the real file aside with a .backup.$(date +%Y%m%d%H%M%S) suffix, then apply. Bootstrap already does this for known targets.

Repository layout

stow/                  Stow packages, each mirroring $HOME
  bash/                Bash, Readline (.inputrc), Starship, functions and aliases
  git/                 .gitconfig and global gitignore
  rex/                 Rex init.lua and the Lua modules it requires
  ghostty/             Fallback terminal config
  herdr/               Herdr config, plugin list, Plannotator TUI config
  nvim/                Neovim config (see stow/nvim/AGENTS.md)
  bin/                 ~/.local/bin tools and shared agent guardrail scripts
  claude/              Claude Code settings, hooks, theme, status line, mods/
  codex/               Codex config, hooks, theme
  pi/                  Pi settings, MCP config, models, themes, extensions/
  agents/              Shared skill catalog under ~/.agents/skills/
scripts/
  bootstrap.sh         Full machine setup
  stow.sh              apply / dry-run / delete, with isolated preflight
  validate-dotfiles.sh Validation without live changes
  backup.sh            Capture live config into tracked sources
  sync_herdr_plugins.sh Install/update declared Herdr plugins
  update_agent_skills.sh Check or sync the shared skill catalog
  rex_server_setup.sh  Root-only Rex login server setup for agentbox
  rex_prefs.sh         Save / load / auto-save Rex app settings
rex/                   Tracked Rex app settings (not stowed)
server/rex/            Files installed under /etc on agentbox (not stowed)
mcp/                   MCP templates and notes (see mcp/README.md)
test/                  Shell and TypeScript tests, Docker harness
docs/                  Research notes and plans; decisions land here, not in this README
old/                   Archived Fish, Zsh, tmux, Karabiner, Zed, and Cursor config
Brewfile               Homebrew packages and casks
npm-global-packages.txt Global npm packages (@openai/codex, vercel, tree-sitter-cli)

stow/opencode/ exists but is not in the default profile and is not maintained.

Rex

Rex (beta) is the default terminal and multiplexer. Its tracked config is stow/rex/.config/rex/init.lua plus the Lua modules beside it (keys, editor/Cmd-key handling, annotations, Neovim navigation). Bindings in that file also apply to sessions on remote hosts. Rex does not add its CLI directory to PATH, so environment.bash adds $REX_BIN_DIR.

After applying, verify with:

rex config reload
rex keymap   # should load without warnings

Open Neovim once so lazy.nvim installs vim-rex-navigator. With it, Ctrl-h/j/k/l moves between Neovim splits and hands off to the adjacent Rex (or Herdr) pane at a split's edge.

Rex keys

Ctrl-a is the prefix, as in tmux/Herdr. Press Ctrl-a Ctrl-a to send a literal Ctrl-a.

KeysAction
Ctrl-a s, Ctrl-a wSession picker
Ctrl-a N, Ctrl-a $New session, rename session
Ctrl-a (, Ctrl-a )Previous, next session
Ctrl-a c, Ctrl-a n, Ctrl-a p, Ctrl-a 1..9New, next, previous, numbered tab
Ctrl-a ,, Ctrl-a &Rename, close tab
Ctrl-a %, Ctrl-a "Split right, split down
Ctrl-a h/j/k/lFocus pane by direction
Ctrl-a zZoom pane
Ctrl-a RResize mode: h/j/k/l or arrows (Shift for bigger steps), = balance, Enter/Esc to leave
Ctrl-a b, Ctrl-a r, Ctrl-a ?Toggle sidebar, reload config, command palette
Ctrl-a a, Ctrl-a A, Ctrl-a mAnnotate copied selection, copy annotations as agent context, manage annotations
Ctrl-h/j/k/lMove between Neovim splits and Rex panes

Annotations reuse Herdr's herdr-annotate binary through the rex-annotate helper, so keep the Herdr plugin installed with ./scripts/sync_herdr_plugins.sh. Rex has no selection API yet, so annotations read the clipboard: select text first (copy on select), then press Ctrl-a a.

Rex app settings (one time per machine)

Theme, font, and GUI settings live in the app (com.superlogical.rex.plist), not in init.lua. scripts/rex_prefs.sh tracks the Workspace.* settings (themes, font, tabs, cursor, padding) in rex/preferences.plist:

./scripts/rex_prefs.sh save    # copy current Rex settings into the repo
./scripts/rex_prefs.sh load    # restore them (quit Rex first)
./scripts/rex_prefs.sh watch   # LaunchAgent that runs save whenever Rex changes a setting

Window positions, sessions, and server/host state stay machine-local. Set the rest once:

  • ☐ Shell: /opt/homebrew/bin/bash --login.
  • ☐ Terminal: copy on select, Option as Alt, blinking block cursor, padding color matching the background, fade inactive panes.
  • ☐ Notifications: macOS notifications for blocked and finished programs, sounds off.
  • ☐ Rex Server: Remote Control on (rex do and agents need it).
  • ☐ Put Rex in the Dock and login items in place of Ghostty.

Rex server on agentbox

agentbox (Linux, on the tailnet) runs the Rex login server, so sessions keep running while the Mac is closed and survive Rex upgrades. init.lua declares the host, so it shows up in the app and in rex hosts list.

# On agentbox, after installing the rex package from the Rex docs
sudo ~/code/personal/dotfiles/scripts/rex_server_setup.sh
rex skill setup

# From the Mac
rex -S agentbox ls

The setup script installs server/rex/accounts.jsonc and the systemd drop-in, and refuses to run if the packaged ExecStart no longer matches the tracked one. Pin upgrades with sudo apt-mark hold rex when you want to postpone one. Herdr also runs on agentbox because Rex has no public iOS client yet; the phone reaches it through Termius.

Known Rex beta gaps

Things the old Ghostty + Herdr setup had that Rex does not yet:

  • No detach: closing a session's last tab destroys the session, so Ctrl-a q/Ctrl-a d are unbound.
  • No close confirmation for panes or tabs with running programs.
  • No last-pane (Ctrl-a ;) and no keyboard copy mode (Ctrl-a [).
  • No selection API (annotations read the clipboard), no remote image paste, and no dragging screenshot thumbnails into agents.
  • No built-in diff/file viewer: use Neovim diffview (<leader>gD) or hunk.
  • No background opacity/blur, no font-variation, and appearance settings are not file-based.

Neovim

Neovim (>= 0.11) is the IDE. Plugins are one spec per file under stow/nvim/.config/nvim/lua/plugins/, pinned in lazy-lock.json. The load-bearing ones are snacks.nvim (picker, explorer, dashboard, lazygit), fff (Cmd-P file finder), blink.cmp (completion), mason + lspconfig, treesitter, flash.nvim (jumps), gitsigns, diffview, and nvim-spectre (project search/replace). Per-language indentation lives in ftplugin/*.lua. Detailed rules for editing this config are in stow/nvim/AGENTS.md.

Leader is Space. Which-key documents everything in-editor (press Space and wait), so this is only the short list:

KeysAction
<C-p>, <leader>sfFind files (includes hidden and gitignored files)
<leader>sgGrep
<C-n>, <leader>eFile explorer
<C-h/j/k/l>Move between splits, then Rex/Herdr panes at the edge
gd, gr, gI, gyDefinition, references, implementation, type definition
K, <leader>rn, <leader>caHover, rename, code action
gl, [d, ]dDiagnostics float, previous, next
<leader>ggLazygit
<leader>gDDiffview
]h, [hNext, previous git hunk
<leader>tt, <leader>tf, <leader>tsTest nearest, file, suite
<leader>a (visual)Annotate selection (Rex/Herdr)
<leader>uCToggle secret masking for .env-style files (display only)

In Rex, Neovim also gets Cursor-style Cmd keys: Cmd-P find files, Cmd-Shift-F search in files, Cmd-B explorer, Cmd-Shift-O symbols, Cmd-S save, Cmd-/ comment.

Mason installs lua_ls, ts_ls, gopls, clangd, and ty (Python). Ruby LSP runs outside Mason so it uses the project's Ruby; install it once with gem install ruby-lsp --bindir "$HOME/.local/bin" --no-document. TypeScript extras: :TSC typechecks into quickfix, ts-error-translator makes diagnostics readable, and :TwoslashQueriesEnable shows inline types.

There is no in-editor debugger. Use tests and logs first, then dlv for Go or debugpy for Python in a terminal pane.

Before changing plugins, run the safety harness; it checks startup in an isolated profile without touching live config or the lockfile:

bash test/nvim_plugin_safety.sh --base-ref HEAD

Shell

Homebrew Bash 5 is the login shell, and Rex, Ghostty, and Herdr all launch /opt/homebrew/bin/bash explicitly. Layout: environment and PATH in ~/.config/bash/environment.bash, aliases in aliases.bash, functions in functions.bash, secrets and machine overrides in the untracked local.bash. Starship renders the prompt, FZF provides completion and Ctrl-F fuzzy history, zoxide provides z, and mise activates per-project runtimes. Generated init scripts for FZF, Starship, and zoxide are cached under ~/.cache/bash/init/ and regenerated when their binaries change.

Agents

Claude Code is the primary harness. Pi is the harness for every other model (its default is an OpenAI model, set in stow/pi/.pi/agent/settings.json; select others with /model). Codex stays configured, but cross-provider review now goes through the council skill: Opus, Sol, and Muse review the same diff in parallel, and code-review calls it for its council step. In Pi, invoke it as /skill:council, since Pi's /council belongs to pi-subagents.

Shared rules and skills

One global rules file, stow/pi/.pi/agent/AGENTS.md, is symlinked into Claude Code and Codex by their Stow packages, so all three harnesses follow the same instructions. Reload or restart sessions after changing it.

Skills live once in stow/agents/.agents/skills/ and are linked into ~/.agents/skills/, which every harness reads. Ownership (upstream baseline, personalization layer, or local) is recorded in .skill-sources.tsv in that folder; see CONTEXT.md for the terms.

./scripts/update_agent_skills.sh --check   # report drift from upstream catalogs
./scripts/update_agent_skills.sh --review  # open a Plannotator review of the diff
./scripts/update_agent_skills.sh --sync    # update the tracked snapshot (uncommitted)

local skills are never overwritten by sync, and excluded skills are never restored. Plannotator's own skills are excluded on purpose because its installer owns them (bootstrap runs it). A new machine gets the tracked snapshot from ./scripts/stow.sh apply with no upstream fetch.

MCP servers

Pi's tracked config (stow/pi/.pi/agent/mcp.json) defines seven servers: Ref (library docs), Exa (web search), Jev (TypeSafe judgments), Chrome DevTools (isolated browser), codex-chrome (your real Chrome profile), HEY (mail/calendar via hey mcp), and superlogical (Rex docs and feedback). Keys come from the environment (REF_API_KEY, EXA_API_KEY, TYPESAFE_API_KEY), never from tracked files. Run pi mcp list to check connections.

To give Claude Code the same servers, import them into its machine-local config:

./mcp_setup.sh install-claude

Codex declares its servers in stow/codex/.codex/config.toml. The legacy template flow in mcp/ is documented in mcp/README.md.

Two things worth knowing:

  • codex-chrome uses the community codex-control-chrome-mcp bridge to drive your signed-in Chrome through the Codex extension. It has full page access and no per-site permissions. Install with npm install -g codex-control-chrome-mcp and codex-control-chrome-mcp install-native-host --browser chrome; uninstall-native-host undoes it. Chrome DevTools is the isolated alternative.
  • HEY has read/write access to mail, calendars, todos, and contacts. Change its args to ["mcp", "--read-only"] in mcp.json to limit it to reads.

Guardrails

Shared scripts in stow/bin/.local/share/agent-guardrails/ block dangerous shell commands and hand edits to generated files. Claude Code and Codex run them as hooks; Pi runs them from its agent-guardrails extension. Rails extras: schema and lockfile edits are blocked, destructive db: tasks require RAILS_ENV=test, and production Rails commands are blocked. Pi also screens fetched web content and audits edit scope with Jev (jev-guardrails extension), and Claude Code does the equivalent through its jev-* mods. Test the shell rules with bash test/agent_guardrails_test.sh "$PWD".

Planning and review

Plans are project Markdown files, reviewed in chat, Plannotator TUI (plannotator-tui <file>), or Plannotator's browser UI, and implemented only after explicit approval. Ctrl-a Shift-o and Ctrl-a Shift-l review a folder or an agent's last reply in Herdr; in Rex, run plannotator-tui directly.

Debugging Pi requests

pi-log   # Pi with the request logger enabled for that process only

Logs land in ~/.pi/agent/logs/requests/ with owner-only permissions. They contain full prompts and tool results, so review before sharing and delete when done (rm -rf ~/.pi/agent/logs/requests).

Extra tools in bin

  • anti-slop runs a shared Oxlint rule set (Dillon Mulroy's rules, vendored unmodified; see stow/bin/.local/share/anti-slop/UPSTREAM.md) against any JS/TS project without per-project setup: anti-slop ., anti-slop --effect . for Effect rules, anti-slop --fix . to autofix. Bootstrap installs its runtime; validate with bash test/global_anti_slop_test.sh.
  • rex-annotate backs the Rex annotation keys described above.
  • spotify-visualizer renders a terminal dot-matrix visualizer driven by the current Spotify track. It needs a Spotify developer app with redirect URI http://127.0.0.1:8974/callback and SPOTIFY_CLIENT_ID exported; tokens cache under ~/.cache/dotfiles/spotify-visualizer/. Keys: Space play/pause, n/p next/previous, s shuffle, r repeat, q quit.

Herdr and Ghostty (fallbacks)

Ghostty (stow/ghostty/) is the fallback terminal; its config is what Rex's theme was imported from. Shift-Cmd-click opens links there despite application mouse capture.

Herdr (stow/herdr/) shares Rex's Ctrl-a mental model and still matters in two places: its annotate plugin powers Rex annotations, and it is the phone path into agentbox. Useful commands: herdr server reload-config, herdr integration status, herdr pane list, herdr agent list. Keep its plugins current with ./scripts/sync_herdr_plugins.sh, which installs plannotator/herdr-annotate and paulbkim-dev/vim-herdr-navigation from the tracked plugins.txt. Install agent integrations once per machine with herdr integration install pi or herdr integration install codex.

Validation

./scripts/validate-dotfiles.sh                 # full isolated preflight, no live changes
bash -n scripts/*.sh stow/bash/.bashrc         # shell syntax spot check
bash test/run_tests.sh                         # shell test suite
npm --prefix test test                         # Pi extension tests
npm --prefix test run typecheck                # Pi extension types
bash test/nvim_plugin_safety.sh --base-ref HEAD
(cd stow/nvim/.config/nvim && nvim --headless -c "luafile init.lua" -c "qa") 2>&1 | rg -i error
docker build -t dotfiles-test -f test/Dockerfile . && docker run --rm dotfiles-test  # closest to CI

AGENTS.md lists the full preflight and per-area checks.

Troubleshooting

Stow reports conflicts. A real file sits where a symlink goes. Move it aside with a .backup.$(date +%Y%m%d%H%M%S) suffix and restow, or run bootstrap, which backs up known targets first.

Edits to ~/.config/... do not show in git. Run readlink on the path; if it does not point into this repo, the package is not applied. Restow that package.

Neovim LSP or treesitter trouble. Needs Neovim >= 0.11 and tree-sitter-cli >= 0.26.1. Check :Mason, :LspInfo, and :checkhealth nvim-treesitter.

Markdown crashes Neovim with CODESIGNING Invalid Page. Stale parsers in ~/.local/share/nvim/site/parser (even markdown.so.disabled) get loaded and killed by macOS. Move them out of that directory and retry.

Prompt looks plain after an update. Run brew bundle --file=Brewfile, then ./scripts/stow.sh apply, then exec /opt/homebrew/bin/bash --login.

Herdr config or agent state looks stale. Confirm readlink ~/.config/herdr/config.toml points into stow/herdr/, run herdr server reload-config, and check herdr integration status. Herdr keeps the previous keymap when bindings are invalid; details are in ~/.config/herdr/herdr-server.log.

Source 1 files
hooks/register.ts 86 lines
1import type { Register } from 'claude-code'
2
3const TOOL_NAME = 'xai_search'
4// The Pi extension owns the xAI auth guards and request bounds; this mod only adapts its runner.
5const RUNNER = '../../../../pi/.pi/agent/extensions/xai-search/run-xai-search.ts'
6// Pinned so a project's mise Node version cannot pick a runtime without native TypeScript.
7const NODE = '/opt/homebrew/bin/node'
8// The runner's own request timeout is 300 s.
9const RUNNER_TIMEOUT_MS = 330_000
10const RUNNER_FAILED = 'xai_search runner failed'
11
12const handleList = (description: string) => ({
13  type: 'array',
14  items: { type: 'string', minLength: 1, description: 'X handle, with or without a leading @' },
15  minItems: 1,
16  maxItems: 20,
17  uniqueItems: true,
18  description,
19})
20
21const inputSchema = {
22  type: 'object',
23  additionalProperties: false,
24  required: ['query'],
25  properties: {
26    query: { type: 'string', minLength: 1, description: 'What to search for on X (Twitter)' },
27    model: { type: 'string', minLength: 1, description: 'xAI model override; uses the configured model when omitted' },
28    from_date: { type: 'string', description: 'Inclusive start date, YYYY-MM-DD, from 00:00 UTC' },
29    to_date: {
30      type: 'string',
31      description:
32        'Exclusive end date, YYYY-MM-DD, UTC. Posts on this date are not included; to search one day, set to_date to the following day.',
33    },
34    allowed_x_handles: handleList('Only consider posts from these X handles (maximum 20)'),
35    excluded_x_handles: handleList('Exclude posts from these X handles (maximum 20)'),
36    enable_image_understanding: { type: 'boolean', description: 'Allow xAI to analyze images in X posts' },
37    enable_video_understanding: { type: 'boolean', description: 'Allow xAI to analyze videos in X posts' },
38    max_turns: { type: 'integer', minimum: 1, description: 'Maximum xAI assistant/tool-call turns for this search' },
39    max_output_tokens: { type: 'integer', minimum: 1, description: 'Maximum output tokens for the final answer' },
40  },
41}
42
43const PARAM_NAMES = Object.keys(inputSchema.properties)
44
45function failed(text: string) {
46  return { isError: true as const, result: text, text }
47}
48
49export const register: Register = on => {
50  on('session.start', async ($, e, next) => {
51    await $.tool.register({
52      name: TOOL_NAME,
53      description:
54        "Search live X (Twitter) posts with xAI's x_search over Pi's SuperGrok/X Premium sign-in. Supports date ranges (to_date is exclusive), handle filters, image/video understanding, and turn/output limits.",
55      inputSchema,
56    })
57
58    return next(e)
59  })
60
61  on('tool.call', { tool: 'mcp__xai-search__xai_search' }, async ($, e) => {
62    const call = e as unknown as Record<string, unknown>
63    const params = Object.fromEntries(PARAM_NAMES.filter(name => call[name] !== undefined).map(name => [name, call[name]]))
64
65    const run = await $.process
66      .run([NODE, `${$.plugin.root}/${RUNNER}`, '--interactive', '--params', '/dev/stdin'], {
67        stdin: JSON.stringify(params),
68        timeoutMs: RUNNER_TIMEOUT_MS,
69      })
70      .catch(() => undefined)
71    if (!run) return failed(RUNNER_FAILED)
72    // The runner's errors are already sanitized of credential detail.
73    if (run.exitCode !== 0) return failed(run.stderr.trim() || RUNNER_FAILED)
74
75    let output: { text?: unknown }
76    try {
77      output = JSON.parse(run.stdout)
78    } catch {
79      return failed(RUNNER_FAILED)
80    }
81    if (typeof output.text !== 'string') return failed(RUNNER_FAILED)
82
83    return { result: output.text }
84  })
85}
86