Skip to content

plyght/qs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

qs

A custom coding agent built on top of @mariozechner/pi-coding-agent. Curates a fixed set of pi extensions, owns its own customization layer, and ships a single binary you control.

Why

pi is great but the extension ecosystem has two pain points:

  1. Updates break customizations. Every pi update rolls extensions forward and can clobber any tweaks you made to make them work together.
  2. Settings are scattered. Per-extension JSON, ~/.pi/agent/settings.json, project .pi/settings.json, env vars, CLI flags. No single source of truth.

qs solves both by being a thin Bun project that:

  • Pins every extension as a normal package.json dependency. Updates are deliberate bun update <pkg>, never automatic.
  • Wraps each extension factory in a customize() seam so your tweaks live in your config file, not in vendored source. They survive upstream updates.
  • Loads extensions as inline factories through pi-coding-agent's SDK, with no filesystem discovery at all (no jiti recompile, no walks). All extensions are imported statically and bundled by Bun at build time.
  • Replaces pi-fff entirely with a qs-owned override that uses @ff-labs/fff-node directly with a shared FileFinder per indexing root (kills the per-call respawn that was the actual perf bug).
  • Adds find_files_all via fd --no-ignore --hidden for the "I want files inside node_modules / .git" case that fff's binary refuses to do (gitignore is hardcoded into the Rust core; not a config option).
  • Provides one typed config file at ~/.config/qs/config.ts and a runtime patches dir at ~/.config/qs/patches/ for AI-written overrides.
  • Vendors your custom footer into extensions/footer.ts so the bottom bar matches your existing pi setup.

What's curated

Extension Source Status
pi-subagents npm
pi-subagents/notify npm
pi-web-access npm
@burneikis/pi-vim npm ⚠️ resolves at runtime, see "Known issues"
pi-rewind npm
@aliou/pi-processes npm ⚠️ second-order import broken (see below)
pi-btw npm ⚠️ second-order import broken
pi-ask-user npm ⚠️ second-order import broken
@samfp/pi-memory npm ⚠️ uses better-sqlite3 (Bun-incompatible) AND second-order import broken
pi-free (11 providers) git: github:apmantza/pi-free
pi-collapse-tools npm ⚠️ second-order import broken
qs/extensions/fff.ts qs-owned ✅ replaces upstream pi-fff
qs/extensions/footer.ts qs-owned ✅ vendored from ~/.pi/agent/extensions/footer.ts

pi-coding-agent itself is a dependency, not vendored. You get core engine improvements automatically; only the extension ecosystem is locked.

Layout

~/qs/
  package.json              # all deps pinned, bun-only, no npm
  tsconfig.json
  bin/qs                    # bun shim entry point
  src/
    main.ts                 # boot: createAgentSessionRuntime + InteractiveMode
    config.ts               # typed QsConfig schema + loader
    extensions.ts           # registry, applies tool middleware + customize seam
    tool-middleware.ts      # Proxy wrapper that intercepts pi.registerTool
    paths.ts                # XDG-style ~/.config/qs, ~/.local/share/qs, ~/.cache/qs
    patches.ts              # discovers ~/.config/qs/patches/*.ts at runtime
    qs-builtin.ts           # /qs-doctor /qs-version /qs-paths /qs-init /qs-patches
    types/extension-modules.d.ts  # ambient declarations for npm .ts subpaths
  extensions/               # ALL qs-owned extensions live here
    _upstream.ts            # @ts-nocheck shim, static imports of all npm extensions
    fff.ts                  # qs's fff override (replaces pi-fff)
    footer.ts               # custom bottom bar
  qs.config.example.ts      # copy to ~/.config/qs/config.ts
  dist/                     # bun --compile output + runtime assets
~/.config/qs/
  config.ts                 # your customizations (auto-loaded if present)
  patches/                  # *.ts files auto-loaded as inline extensions
~/.local/share/qs/
  sessions/                 # session history
  auth.json                 # credentials
  models.json               # model registry
~/.cache/qs/
  fff/                      # fff frecency + history DBs

Architecture decisions

Why not vendor every extension's source? ~180 files. Updates would become manual diffs. Worst of all worlds.

Why not just use upstream pi with a config layer? That's what broke before. Updates rolled extensions forward and your tweaks died.

Why static imports in extensions/_upstream.ts instead of dynamic import()? Bun's --compile only bundles statically-resolvable imports. Earlier iterations used dynamic-string import(specifier) to dodge TypeScript's third-party type checking, but those weren't bundled into the binary, so the compiled qs failed to find extensions at runtime. Static imports + a // @ts-nocheck shim file + a filtered tsc script is the fix. Third-party .ts source still floods tsc with errors (each upstream extension was published against a different version of pi's internal types), but those errors are filtered out and don't affect runtime.

Why is fff hardcoded to respect .gitignore? Because @ff-labs/fff-node@0.5.2's InitOptions has no flag to disable it. The fff Rust binary enforces gitignore filtering. The README on dmtrKovalenko/fff.nvim confirms this. Two tools handle the split: find_files (fff, ranked, gitignored) and find_files_all (fd --no-ignore --hidden, slower on huge repos but actually sees everything).

Why ~/.local/share/qs/ instead of ~/.pi/? XDG Base Directory spec. Cleaner separation from pi's data so they don't share or fight over session files, auth, or settings.

Why a patches dir? When an upstream extension breaks under a new pi version, you (or AI) drop a .ts file into ~/.config/qs/patches/. It's loaded as an inline extension factory after the registry, so it can register tools with the same names (later registration wins) and override behavior without touching vendored source. No fork, no rebuild, no bun update collision. The "AI repatches it" use case from the spec.

How to use

First run

cd ~/qs
bun install
bun run build           # compiles dist/qs + copies runtime assets
./dist/qs               # launch

Or skip the binary and use the dev shim:

cd ~/qs
bun dev                 # bun --watch run src/main.ts

Configure

~/qs/dist/qs            # launch
> /qs-init              # writes a starter ~/.config/qs/config.ts

Then edit ~/.config/qs/config.ts. Anything you set there merges over DEFAULT_CONFIG from src/config.ts. The customize field gives you a typed (api: ExtensionAPI) => void callback per extension key, called after the upstream factory loads, so you can register additional tools, wrap commands, etc.

Disable an extension

extensions: {
  memory: false,        // skip @samfp/pi-memory entirely
}

Disable a specific tool from any extension

tools: {
  disable: ["bash", "subagent_v2"],
}

Wrap a tool with middleware

tools: {
  middleware: {
    bash: async (ctx, next) => {
      console.log("about to run bash:", ctx.params);
      const result = await next();
      console.log("bash done");
      return result;
    },
  },
}

The middleware proxy is wired through wrapApiWithPolicy in src/tool-middleware.ts. It intercepts pi.registerTool calls during factory execution and wraps the execute field. It applies to extension-registered tools — built-in bash/edit/write (passed via tools array to pi's runtime) are not yet middleware-wrapped (iteration 3 work).

Drop a runtime patch

mkdir -p ~/.config/qs/patches
cat > ~/.config/qs/patches/01-fix-broken-tool.ts <<'EOF'
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";

export default function (pi: ExtensionAPI) {
  pi.registerTool({
    name: "broken_tool_name",   // same name as upstream registers — overrides it
    label: "fixed",
    description: "qs replacement for a broken upstream tool",
    parameters: Type.Object({}),
    execute: async () => ({
      content: [{ type: "text", text: "ok" }],
      details: undefined,
    }),
  });
}
EOF

Restart qs. /qs-patches will list it. /qs-doctor will show every loaded factory and tool name so you know what to patch.

Built-in slash commands

Command Purpose
/qs-doctor Full diagnostic: extensions loaded, tool inventory, paths, errors
/qs-version qs version
/qs-paths config, data, cache locations
/qs-init scaffold a starter ~/.config/qs/config.ts
/qs-patches list active runtime patches and patch errors

Known issues (state at handoff)

  1. Second-order module resolution failures. Several extensions fail to load at runtime with errors like:

    pi-btw: Cannot find module '@mariozechner/pi-coding-agent' from '/Users/nicojaffer/qs/node_modules/pi-btw/extensions/btw.ts'
    pi-ask-user: same
    pi-collapse-tools: same
    @aliou/pi-processes: Cannot find '@mariozechner/pi-ai'
    @samfp/pi-memory: Cannot find '@sinclair/typebox'
    

    This happens because the static-import bundle in extensions/_upstream.ts reaches the entry file, but those entries import peer dependencies via bare specifiers that Bun resolves from the importing file's location (inside node_modules/<pkg>/). The peer deps ARE installed at ~/qs/node_modules/, but Bun's bundler isn't following them transitively for some reason — possibly because they're peerDependencies declared by the extension package without listing @mariozechner/pi-coding-agent in its own dependencies.

    Fix path for next iteration: add the peer deps explicitly to qs's package.json so they hoist (@mariozechner/pi-ai, @sinclair/typebox are already there but maybe not at top level — verify with ls node_modules/@mariozechner/). Or use Bun's --external to tell the bundler to keep them as runtime imports resolved from qs's own node_modules. Or bun patch each broken extension to make their imports relative.

  2. @samfp/pi-memory uses better-sqlite3 which is not Bun-compatible. Bun runtime errors out on import Database from "better-sqlite3". Fix: bun patch @samfp/pi-memory to swap better-sqlite3bun:sqlite (the API is mostly compatible — same prepared statements, same .run()/.all()/.get()).

  3. tsc --noEmit is noisy with third-party errors. The typecheck script in package.json filters them out via grep, but the IDE will still flag them. They don't affect runtime. The right long-term fix is project references or moving _upstream.ts to its own sub-tsconfig with noCheck.

  4. Compiled binary needs runtime assets next to it. bun run build already handles this via the copy-assets script — pi's theme JSONs, photon WASM, export-html templates, and a stub package.json get copied to dist/. If you run dist/qs and it ENOENTs, you forgot to run copy-assets.

How to continue

The Ralph loop is set up to keep iterating on this. Each iteration should ship one focused improvement.

Open work in priority order:

  1. Fix the second-order resolution failures. This is the biggest functional gap — 5 extensions are dead at runtime. Investigation: run bun pm ls @mariozechner/pi-coding-agent to see if it's hoisted; if not, force-hoist via package.json resolutions or workspaces. Try bun build --external @mariozechner/pi-coding-agent --external @mariozechner/pi-ai --external @sinclair/typebox and verify that helps. If yes, bake those into the build script.

  2. bun patch @samfp/pi-memory to make memory work on Bun. The patch is small: one import line. Do bun patch @samfp/pi-memory, edit node_modules/@samfp/pi-memory/src/store.ts to swap import Database from "better-sqlite3"import { Database } from "bun:sqlite", then bun patch --commit @samfp/pi-memory. Bun will re-apply it on every install. Verify memory_remember / memory_search tools work end-to-end.

  3. Built-in tool middleware. The current middleware system only wraps extension-registered tools (via wrapApiWithPolicy). Built-in bashTool/editTool/writeTool are passed directly to pi's runtime via the tools array in main.ts and are not wrapped. To fix: write a wrapBuiltinTool(tool, middleware) that returns a new Tool whose execute calls the middleware, and apply it in selectBuiltinTools (already in src/dedup.ts though now unused — wire it back).

  4. Hot reload. A /qs-reload command that re-runs loadConfig(), rebuilds the factories, and swaps them in without restart. Pi's runtime supports refresh via runtime.refreshTools() — investigate whether full extension reload is possible or if it needs a session restart.

  5. Per-project config. .qs/config.ts in cwd merges over ~/.config/qs/config.ts. Useful for project-specific extension toggles (e.g., enable rust-analyzer wrapper only in Rust projects).

  6. Drop the binary, use a Bun shim. 69 MB compiled binary is overkill. Replace with bin/qs shim that does cd $QS_ROOT && exec bun run src/main.ts "$@". Faster builds, no asset copy, same startup time. Or keep both and let users pick.

  7. Auto-discovery of extensions/*.ts. Currently _upstream.ts and extensions.ts have a hardcoded REGISTRY. Use import.meta.glob("./extensions/*.ts") (Bun supports it) so dropping a new file in extensions/ auto-registers it. Lower the friction for "code my own agent" to one file create.

  8. Lockfile-aware updates. bun update --interactive per extension, with a qs-update command that lists which extensions have available upgrades. Right now updates are ad-hoc.

  9. Skill / prompt template / theme passthroughs. qs only wires extensions from pi's resource loader. Skills, prompt templates, and themes are still loaded from pi's default discovery (~/.pi/agent/...). Mirror them into ~/.config/qs/{skills,prompts,themes}/ so qs is fully self-contained.

Useful commands

cd ~/qs

bun dev                                    # watch mode dev launch
bun run build                              # compile + copy assets
bun typecheck                              # filtered tsc (qs-only errors)
./dist/qs                                  # run compiled binary
bun pm ls --depth 1                        # see installed deps
bun update <pkg>                           # bump one extension
bun patch <pkg>                            # start patching an upstream pkg
bun patch --commit <pkg>                   # save the patch

Conventions

  • extensions/ is for all qs-owned extension source (custom + the static-import shim for upstream). One place. No more src/overrides/.
  • src/ is for qs runtime infrastructure only — config, paths, the wrapping system, the entry point, slash commands.
  • Bun-only. Never npm install, never npm run. The package.json scripts use bun.
  • No comments unless they encode a non-obvious why. No banners, no headers, no attribution.
  • XDG paths. ~/.config/qs, ~/.local/share/qs, ~/.cache/qs. Never write to ~/.pi.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors