SLOPSHOPPER

websearch-perplexity

Routes the WebSearch tool to Perplexity (pplx CLI) via a function hook; falls back to native search on failure

newguardprocess
A shopper browsing a rack in a slop shop
README

dotfiles

Personal dotfiles using GNU Stow and XDG Base Directory Specification.

Installation

Clone the dotfiles repository to your home directory:

git clone git@github.com:riethmayer/dotfiles $HOME/dotfiles
cd ~/dotfiles
mise run install

Package Management

Stow Packages

The following tools are managed via GNU Stow:

PackageDescriptionXDG Compliant
agentsShared AI agent instructions and personal-skill symlinks✅
atuinShell history sync✅
bootstrapSystem setup scriptsN/A
brewHomebrew bundleN/A
claudeClaude AI assistant config✅
ghosttyGhostty terminal config✅
herdrHerdr agent workspace manager (sidebar rows, keys, theme)✅
gitGit configuration✅
kittyKitty terminal config✅
miseMise (rtx) config✅
opencodeOpenCode AI config✅
pnpmPNPM configuration✅
rubyRuby/Gem configuration✅
scriptsCustom shell scriptsN/A
sshSSH configuration❌ (hardcoded)
starshipStarship prompt✅
tmuxTmux configuration✅

| zsh | Zsh configuration | ✅ |

Separate Repositories

Excluded Tools

  • gcloud: Google Cloud SDK config excluded (contains credentials)
  • gpg: Cannot be stowed (hardcoded ~/.gnupg path)

Available Commands

mise run help           # Show available tasks
mise run install        # Install all stow packages
mise run bootstrap      # Full system setup
mise run quick-bootstrap # Minimal dev setup (mise, atuin, zsh-xdg, LSP)
mise run lsp            # Install LSP servers (bash, lua, yaml, toml, json, md)
mise run skills         # Install/update personal skills for Claude and Codex
mise run update         # Sync and update dotfiles

Personal Skills

Personal agent skills live in ~/skills and reach each agent through its own marketplace install (Claude: jan-* plugins, Codex: jan-skills marketplace) — content is duplicated per agent, no shared symlink. mise run install prepares the checkout before stow; mise run skills registers the Git-backed Codex jan-skills marketplace and a LaunchAgent that fast-forwards the checkout hourly when it is clean.

Pi prompt stash

stow/pi/.pi/agent/extensions/prompt-stash.ts adds a global Ctrl+S shortcut. Run /reload in an existing Pi session after installing the file.

  1. Write part of a prompt and press Ctrl+S to hide and remember it.
  2. Type a side question and send it. The original draft returns to the editor immediately at input processing, without being submitted or waiting for a reply.
  3. Alternatively, press Ctrl+S on an empty editor to restore it manually.

Works for normal prompts, steering (Enter while working), and follow-ups (Alt+Enter). A footer hint stays visible while a draft is stashed. There is one stash slot; pressing Ctrl+S with another draft warns rather than replacing it. Restoration never overwrites non-empty input.

Drafts stay in process memory, including across /reload and session switches; quitting or crashing Pi loses them. No unsent draft is written to session history or disk. This is text-only: pasted text is preserved in full, but cursor position, collapsed-paste presentation, and separately managed image attachments are not captured. Built-in/extension commands bypass the input hook: use Ctrl+S to restore manually after those. Input queued during compaction restores when Pi processes it.

Verify with mise run pi:test (Node 22.6+; no dependencies or model calls).

Cron

cron/crontab.txt is the source of truth for non-factory cron entries — install with crontab ~/dotfiles/cron/crontab.txt. Factory-owned entries self-install afterwards from their owning repo (marker-managed blocks, idempotent):

node ~/skills/scripts/reap-agents.mjs --install-cron   # herdr idle-agent reaper (15min)

Directory Structure

dotfiles/
├── stow/              # GNU Stow packages
│   └── {tool}/        # Per-tool configurations
├── .planning/         # Sprint-based improvements
├── .rules/            # Shared rules for AI assistants
└── .mise.toml         # Task runner configuration

Acknowledgments

Thanks to @andrzejsliwa and @sevos for the inspiration.

Source 1 files
hooks/websearch.ts 84 lines
1// WebSearch → Perplexity. Answers each WebSearch call with `pplx search web`
2// in WebSearch's own result shape. Any failure logs a warning and falls through
3// to next(e), so the native search runs instead. Key: PERPLEXITY_API_KEY, else
4// `op read` through the machine's 1Password service account (dotfiles ADR-010),
5// once per session. PPLX_WEBSEARCH=0 turns it off.
6import type { Register } from "claude-code";
7
8type Hit = { url: string; title: string; snippet?: string; summary?: string };
9
10const LIMIT = 8;
11const SNIPPET_CHARS = 700;
12const KEY_REF = "op://machines/Perplexity API Key/credential";
13const TOKEN_KEYCHAIN_SERVICE = "op-service-account-token";
14
15let apiKey: string | undefined;
16
17export const register: Register = (on) => {
18  on("tool.call", { tool: "WebSearch" }, async ($, e, next) => {
19    if ((await $.env.get("PPLX_WEBSEARCH")) === "0") return next(e);
20    const fallback = (reason: string) => {
21      $.ui.log(`⚠ websearch-perplexity: ${reason}; using Claude's WebSearch`);
22      return next(e);
23    };
24
25    const home = (await $.env.get("HOME")) ?? "";
26    apiKey ??= (await $.env.get("PERPLEXITY_API_KEY")) || undefined;
27    if (!apiKey) {
28      let token = await $.env.get("OP_SERVICE_ACCOUNT_TOKEN");
29      if (!token) {
30        const kc = await $.process
31          .run(["security", "find-generic-password", "-s", TOKEN_KEYCHAIN_SERVICE, "-w"])
32          .catch(() => undefined);
33        if (kc?.exitCode === 0) token = kc.stdout.trim();
34      }
35      if (!token) return fallback(`no 1Password service account token (Keychain ${TOKEN_KEYCHAIN_SERVICE})`);
36      const read = await $.process
37        .run(["op", "read", KEY_REF], { env: { OP_SERVICE_ACCOUNT_TOKEN: token }, timeoutMs: 15_000 })
38        .catch(() => undefined);
39      if (read?.exitCode !== 0) return fallback(`no Perplexity key: op read ${KEY_REF} failed ${read?.stderr.trim().slice(0, 160) ?? ""}`);
40      apiKey = read.stdout.trim();
41    }
42    if (!apiKey) return fallback("no Perplexity key: op read returned nothing");
43
44    const argv = [`${home}/.local/bin/pplx`, "search", "web", e.query, "-n", String(LIMIT), "--max-tokens-per-page", "400"];
45    if (e.allowed_domains?.length) argv.push("--domains", e.allowed_domains.join(","));
46    if (e.blocked_domains?.length) argv.push("--excluded-domains", e.blocked_domains.join(","));
47
48    const started = Date.now();
49    const run = await $.process
50      .run(argv, { env: { PERPLEXITY_API_KEY: apiKey }, timeoutMs: 45_000 })
51      .catch(() => undefined);
52    if (!run) return fallback("pplx did not run or timed out");
53    if (run.exitCode !== 0) return fallback(`pplx failed: ${run.stderr.trim().slice(0, 200)}`);
54
55    let hits: Hit[];
56    try {
57      hits = (JSON.parse(run.stdout) as { hits?: Hit[] }).hits ?? [];
58    } catch {
59      return fallback("pplx returned unreadable JSON");
60    }
61    if (hits.length === 0) return fallback("Perplexity returned no hits");
62
63    const digest = hits
64      .map((h) => {
65        const text = (h.snippet ?? h.summary ?? "").replace(/\s+/g, " ").slice(0, SNIPPET_CHARS);
66        return `- [${h.title}](${h.url})\n  ${text}`;
67      })
68      .join("\n");
69
70    return {
71      result: {
72        query: e.query,
73        results: [
74          { tool_use_id: "perplexity", content: hits.map((h) => ({ title: h.title, url: h.url })) },
75          `Excerpts:\n${digest}`,
76        ],
77        durationSeconds: (Date.now() - started) / 1000,
78        searchCount: 1,
79      },
80      context: ["These WebSearch results came from Perplexity. Cite sources as markdown links."],
81    };
82  });
83};
84