SLOPSHOPPER

qsm-toolbox

The index of third-party command-line tools on this machine: one skill whose description is the whole list at a fixed always-on cost, a body that carries the…

newprocess
A shopper browsing a rack in a slop shop
README

qsm-toolbox

One list of the command-line tools on this machine, kept in the agent's context, with a doctor that says which are installed, for the person and the agent who share them.

Why it exists

Every agent should know the same set of command-line tools this machine has, and a tool nobody names is a tool nobody uses. qsm-toolbox exists to be that one list, always in the agent's context at a fixed small cost, with the install command, the health probe, and the one caveat --help cannot tell, a single call away. It takes the stance that the index is the product and everything else about a third-party binary belongs upstream. It refuses to document what a tool does; the tool's own help does that. When the harness shows the agent what is on PATH by itself, this plugin has no reason to exist.

What you get

KindNameWhat it does
SkilltoolboxKeeps the whole tool list in context, with the install command, the version probe and one caveat per tool
CLIqsm-toolbox1 verb (doctor), see USAGE.md

Install

claude plugin install qsm-toolbox@qsm-marketplace
bun plugins/qsm-toolbox/scripts/install.ts

The install script compiles qsm-toolbox and links it into ~/.local/bin. It then installs each missing catalog tool through bun, uv, pipx or go. A tool that needs a system package manager or sudo is reported with the line to type and never run. --dry-run prints the plan. More in INSTALL.md.

Use

Ask: "which tool searches code by syntax?" or "is jq installed?". The skill fires and answers from the list. To check the machine yourself:

qsm-toolbox doctor
qsm-toolbox doctor --json

Doctor prints present <bin> <version> or missing <bin> (install: <line>) for each tool. It always exits 0, because every tool is optional.

Caveats

  • Install lines target this machine. Each row names the alternative when a prerequisite is missing.
  • br is installed by the qsm-tasks plugin. Its row here only points there.
  • The qsm CLIs in the second group are listed for discovery. They are never installed or probed here.
  • QSM_<TOOL>_SKIP=1 makes the install script skip one tool.
Source 3 files
hooks/register.tsx 10 lines
1// qsm-toolbox's function-hooks module.
2import type { On } from "claude-code";
3import { registerNoun } from "./noun.generated.ts";
4
5export function register(on: On): void {
6  // Adds $.toolbox: one method per qsm-toolbox verb, generated by lib/generate-noun.ts. Never
7  // hand-edit noun.generated.ts or types/index.d.ts.
8  registerNoun(on);
9}
10
hooks/noun.generated.ts 125 lines
1// Generated by lib/generate-noun.ts from qsm.json. Do not hand-edit; run
2// `bun lib/generate-noun.ts plugins/<name>` again after changing qsm.json.
3import type { Caught, EngineInterface, On } from "claude-code";
4import type { Toolbox } from "../types/index.d.ts";
5
6/** How one option becomes argv: a positional value (`flag` null), a bare
7 * boolean flag, or a flag and its value; a list repeats it per element. */
8export interface VerbArg {
9  readonly name: string;
10  readonly flag: string | null;
11  readonly boolean?: true;
12}
13
14export interface VerbRow {
15  readonly words: readonly string[];
16  readonly args: readonly VerbArg[];
17  readonly passthrough?: true;
18}
19
20export const verbs: Readonly<Record<string, VerbRow>> = {
21  doctor: { words: ["qsm-toolbox","doctor"], args: [] },
22};
23
24/** One verb's argv: its words, each option in declaration order, the
25 * passthrough rest after `--`, and `--json` last. */
26export function argvOf(row: VerbRow, options: Readonly<Record<string, unknown>> = {}): string[] {
27  const argv = [...row.words];
28  for (const arg of row.args) {
29    const value = options[arg.name];
30    const values = Array.isArray(value) ? value : [value];
31    for (const one of values) {
32      if (one === undefined || one === false) {
33        continue;
34      }
35      argv.push(...(arg.flag === null ? [] : [arg.flag]), ...(arg.boolean ? [] : [String(one)]));
36    }
37  }
38  const rest = options.rest;
39  if (row.passthrough && Array.isArray(rest)) {
40    argv.push("--", ...rest.map(String));
41  }
42  argv.push("--json");
43  return argv;
44}
45
46/** The binary prints one outcome envelope on stdout under --json:
47 * { code, exit, reason, message, warnings, data }. Data on ok; otherwise the
48 * code and message become the error. A stale or misbehaving binary that
49 * prints something else (not JSON, or JSON with no numeric `exit`) throws
50 * loud here instead of a raw JSON.parse SyntaxError or a silent `undefined`. */
51export function unwrap(
52  argv: readonly string[],
53  result: { readonly exitCode: number; readonly stdout: string; readonly stderr: string },
54  label = argv.slice(0, 2).join(" "),
55): unknown {
56  const text = result.stdout.trim();
57  const stderr = result.stderr.trim().slice(0, 200);
58  const cause = `(exit ${result.exitCode}): ${stderr.length > 0 ? stderr : "no stderr"}`;
59  if (text.length === 0) {
60    throw new Error(`${argv.join(" ")}: printed no envelope ${cause}`);
61  }
62  let parsed: unknown;
63  try {
64    parsed = JSON.parse(text);
65  } catch {
66    parsed = undefined;
67  }
68  if (typeof parsed !== "object" || parsed === null || typeof (parsed as { exit?: unknown }).exit !== "number") {
69    throw new Error(`${argv.join(" ")}: stdout is not an outcome envelope ${cause}: ${text.slice(0, 200)}`);
70  }
71  const envelope = parsed as { code: string; exit: number; message: string; data: unknown };
72  if (envelope.exit !== 0) {
73    throw new Error(`${label}: ${envelope.code}: ${envelope.message}`);
74  }
75  return envelope.data;
76}
77
78/** What every `.catch` does first: a toast and a log for the person. The toast says `fixedReason`
79 * when the cause may carry something the toast must not show, else the cause; the cause goes to
80 * the log and comes back for the caller's own policy. */
81export function failure(
82  $: EngineInterface,
83  event: string,
84  error: Caught["error"],
85  fixedReason?: string,
86): string {
87  const said = error.message ?? error.kind;
88  $.ui.toast(`qsm-toolbox ERROR: ${event}: ${(fixedReason ?? said).slice(0, 120)}`);
89  $.ui.log(`${event} failed (${error.kind}): ${said}`);
90  return said;
91}
92
93/** Adds $.toolbox to the engine. Call once from the plugin's own
94 * `register(on, options)` — never call the engine.create hook's own
95 * `next(e)` result from anywhere but here. */
96export function registerNoun(on: On): void {
97  on("engine.create", async (_$, e, next) => {
98    const beneath = await next(e);
99    const run = async (row: VerbRow, options?: Readonly<Record<string, unknown>>): Promise<unknown> => {
100      const argv = argvOf(row, options);
101      let result: Awaited<ReturnType<typeof beneath.process.run>>;
102      try {
103        result = await beneath.process.run(argv);
104      } catch (error) {
105        // A spawn in a deleted directory fails as ENOENT naming the binary; name the directory. A
106        // check that cannot run leaves the spawn's own error as the cause.
107        const gone = await beneath.session
108          .cwd()
109          .then(async (cwd) => ((await beneath.fs.exists(cwd)) ? null : cwd))
110          .catch(() => null);
111        if (gone !== null) {
112          throw new Error(`${row.words.join(" ")}: the session's working directory ${gone} no longer exists; cd to one that does`);
113        }
114        throw error;
115      }
116      return unwrap(argv, result, row.words.join(" "));
117    };
118    const methods = Object.entries(verbs).map(([method, row]) => [
119      method,
120      (options?: Readonly<Record<string, unknown>>) => run(row, options),
121    ]);
122    return { ...beneath, toolbox: Object.fromEntries(methods) as Toolbox };
123  });
124}
125
types/index.d.ts 13 lines
1// Generated by lib/generate-noun.ts from qsm.json. Do not hand-edit; run
2// `bun lib/generate-noun.ts plugins/<name>` again after changing qsm.json.
3
4export type Toolbox = {
5  doctor(): Promise<unknown>;
6};
7
8declare module 'claude-code' {
9  interface EngineInterface {
10    toolbox: Toolbox;
11  }
12}
13