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…

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.
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.
| Kind | Name | What it does |
|---|---|---|
| Skill | toolbox | Keeps the whole tool list in context, with the install command, the version probe and one caveat per tool |
| CLI | qsm-toolbox | 1 verb (doctor), see USAGE.md |
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.
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.
br is installed by the qsm-tasks plugin. Its row here only points there.QSM_<TOOL>_SKIP=1 makes the install script skip one tool.hooks/register.tsx 10 lines1// 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}
10hooks/noun.generated.ts 125 lines1// 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}
125types/index.d.ts 13 lines1// 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