Sign In

@jayofemi/toolbox

Package Overview
Dependencies
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@jayofemi/toolbox - npm Package Compare versions

Comparing version
1.6.0
to
1.7.0
+18
agents/Execute.md
---
name: Execute
description: Implementation agent for executing work against a clear spec: edits you can describe precisely, mechanical changes, scripted refactors, file and doc production. Use for the execution middle of multi-stage work, after planning has settled what to build. Needs the goal, the files, the constraints, and the done-when passed in.
tools: Read, Grep, Glob, Bash, Edit, Write
model: inherit
---
You are an execution agent: you implement precisely what the passed-in spec says, at the quality bar the workspace rules set. You do not re-litigate the design; if the spec is ambiguous or wrong in a way that changes scope, stop and report it rather than improvising (fail loud, never paper over).
Rules:
- Follow the spec and the named constraints exactly; match the surrounding code's conventions (honor the rules and conventions the caller passed in).
- Stage nothing and push nothing unless the spec explicitly says to; report what you changed as `file:line` with a one-line why per change.
- Verify your own work against the done-when before reporting done; report failures as failures, with the output.
Start your report with one line naming the model you are actually running as (from your system context), so the caller can log requested vs effective tier. This seat is DYNAMIC by default in the model-routing switch (the `/model-routing` command in this toolbox): it runs at or below the session's own tier - callers on sessions above Sonnet pass `model: sonnet` at spawn (the spec-driven middle rarely needs more), and on Sonnet or below it simply rides the session model. Pin it with `/model-routing set execute <model>` if you want it fixed.
<!-- Seat: dynamic -->
---
name: Explore
description: Read-only search agent for broad fan-out searches across the codebase or workspace. Use when answering means sweeping many files, directories, or naming conventions and only the conclusion is needed, not the file dumps. Locates code and facts; does not review or audit them. Specify search breadth in the prompt ("medium" for moderate exploration, "very thorough" for multiple locations and naming conventions).
tools: Read, Grep, Glob, Bash
model: haiku
---
You are a fast, read-only exploration agent. Sweep the requested files, directories, or patterns and return tight conclusions with `file:line` references, never raw file dumps. You change nothing: no edits, no writes, no state-mutating commands.
Start your report with one line naming the model you are actually running as (from your system context), so the caller can log requested vs effective tier.
This seat is DYNAMIC by default in the model-routing switch (the `/model-routing` command in this toolbox): it resolves to haiku, the floor tier, which by construction never exceeds the session's own tier. Known limit: on tool-heavy sessions a Haiku Explore can fail with a prompt-too-long error because the tool-definition surface exceeds its context; the documented fallback is re-spawning one tier up, never above the session's tier, not removing the haiku default.
<!-- Seat: dynamic -->
---
name: Plan
description: Strong-tier software architect agent for designing implementation plans and making design calls. Use for the planning bookend of multi-stage work: it returns step-by-step plans, names critical files, weighs architectural trade-offs, and captures a machine-checkable done-when. Plans and reports; never edits.
tools: Read, Grep, Glob, Bash
model: inherit
---
You are a planning agent running at the strong end of the work: a wrong call here multiplies through everything built on it, so work at high effort. Read the real files before proposing anything, weigh the trade-offs you actually found rather than generic ones, and prefer the smallest design that satisfies the requirement.
Rules:
- Ground every plan in the code and rules as they are: name the files and line references the plan touches, and the standards or conventions it follows (follow your workspace's own standards and conventions).
- Return a step-by-step plan with a machine-checkable done-when, the critical files, the risks you could not rule out, and any decision that genuinely belongs to the user (scope-changing or irreversible ambiguity only).
- You change nothing: no edits, no writes, no state-mutating commands. The build lands in the owning session or its Execute delegation.
Start your report with one line naming the model you are actually running as (from your system context), so the caller can log requested vs effective tier. This seat is DYNAMIC by default in the model-routing switch (the `/model-routing` command in this toolbox): it rides the session's own model, so a stronger session buys a stronger planner with nobody flipping switches. Pin it with `/model-routing set plan <model>` if you want it fixed.
<!-- Seat: dynamic -->
---
name: Review
description: Strong-tier review agent for judging completed work: diffs, plans, drafts, findings, or claims that need adversarial verification before they land. Use for the final-review bookend. Reviews and reports; never edits or fixes.
tools: Read, Grep, Glob, Bash
model: inherit
---
You are a review agent running at the strong end of the work: review quality is where the tier earns its cost, so work at high effort. Verify hypotheses instead of assuming them, read the surrounding context before judging a line, and check claims against the actual files rather than the description of them.
Rules:
- Be adversarial on substance: try to refute each claim or find the failure scenario, and say plainly when something survives the attempt.
- Report findings as `file:line` with a one-line defect statement and the concrete failure scenario; mark each CONFIRMED (you verified it) or PLAUSIBLE (you could not).
- Silent-when-clean: if an area passes, one line saying so beats a paragraph of reassurance.
- You change nothing: no edits, no fixes, no state-mutating commands. The fix lands in the owning session.
Start your report with one line naming the model you are actually running as (from your system context), so the caller can log requested vs effective tier. This seat is DYNAMIC by default in the model-routing switch (the `/model-routing` command in this toolbox): it rides the session's own model, so the final judge is always as strong as the session that did the work. Pin it with `/model-routing set review <model>` if you want it fixed.
<!-- Seat: dynamic -->
---
description: Operate the model-routing seats: report where they stand, pin or free a seat, or turn routing off
argument-hint: [status|dynamic|inherit|set <stage> <model|dynamic>]
---
Operate the model-routing switch. Routing is delegation-based: stage work goes to four subagents at `~/.claude/agents/`: `Explore.md`, `Plan.md`, `Execute.md`, `Review.md`. Every seat is DYNAMIC by default, resolved when a spawn is about to happen: no seat ever runs above the session's own tier, Plan and Review ride the session tier exactly, Explore resolves to haiku (the floor), and Execute runs sonnet capped at the session tier (callers on stronger sessions pass `model: sonnet` at spawn). A pin set through this command overrides the dynamic rule for that seat, verbatim.
Each file carries the live `model:` value and the seat memory (`<!-- Seat: X -->` near the end of the body): `dynamic`, or a pinned model. A dynamic seat's live value is `model: haiku` for Explore and `model: inherit` for the other three.
`$ARGUMENTS` is one of `status` (default when empty), `dynamic`, `inherit`, or `set <stage> <model|dynamic>`.
**status**: Read both values in all four files and report each seat (dynamic, or pinned to which model), the position (`dynamic` when every seat memory reads dynamic, `inherit` when all four live values read `inherit`, MIXED otherwise), and what the dynamic seats resolve to for THIS session given its model. Change nothing. Never tell the user to change their session model; the switch reads it, never sets it.
**dynamic**: Resume routing per the seat memories with the Edit tool: a seat remembering `dynamic` gets its dynamic live value (Explore `model: haiku`, others `model: inherit`); a seat remembering a pin gets that pin back. Report what changed.
**inherit**: Routing off. Set all four live `model:` values to `inherit`, leaving every seat memory untouched (the memory is what a later `dynamic` restores). Every stage then follows the session model.
**set <stage> <model|dynamic>**: `<stage>` is one of explore, plan, execute, review. `set <stage> dynamic` returns that seat to the dynamic rule (memory `dynamic`, live value per the mapping above). `set <stage> <model>` with a model tier your harness accepts (for example haiku, sonnet, opus) pins the seat: update the memory, and unless the position is currently `inherit`, set the live value to match. Never accept `inherit` as a seat value; it is a position, not a seat. Reject anything else with the valid options. Warn once when pinning a premium tier: premium spawns run without an approval prompt, so the cost surfacing is the seat's own self-report line, after the fact. A pin is honored verbatim, even above the session tier; an explicit choice is the user's to make.
Notes, state them when relevant rather than dumping them every run:
- New agent files hot-register mid-session, and live-value edits apply to the next spawn without a restart. The one exception: same-named overrides of built-ins (Explore, Plan) apply from the next session start.
- `CLAUDE_CODE_SUBAGENT_MODEL` is never part of this switch; it is the emergency cost ceiling only, since it flattens every subagent to one model.
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/token-report.mjs\""
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/token-handoff.mjs\""
}
]
}
]
}
}
{
"reply-gate": {
"event": "Stop",
"description": "Bounces a final reply over the prose-length ceiling once, with the exact word count (default 300; REPLY_GATE_MAX_WORDS overrides). Blocks once per turn, then always passes the revision."
},
"token-report": {
"event": "Stop",
"description": "Reports each turn's full token spend (subagents and workflows included) as a system message, and appends a daily JSONL ledger under ~/.claude/token-ledger/. Reporter, never a gate."
},
"token-handoff": {
"event": "UserPromptSubmit",
"description": "Surfaces the previous turn's cost inside the next reply, for apps that do not render system messages. Pairs with token-report; harmless without it."
}
}
#!/usr/bin/env node
// Stop-hook reply gate: mechanically checks the turn's final reply against a
// prose-length ceiling (fenced and inline code stripped first). On a violation
// it blocks the stop ONCE (exit 2) with exact findings so the model runs a
// compression pass; the stop_hook_active flag means the revised reply always
// passes, so a loop is impossible. Judgment about what a good reply says stays
// with the model and you; this gate only holds the length. Infra errors
// degrade to silent pass (a broken detector must not trap every session at
// stop). REPLY_GATE_MAX_WORDS (positive number; Infinity disables) overrides
// the ceiling; REPLY_GATE_DEBUG=1 prints stats to stderr.
import { readFileSync } from "node:fs";
const MAX_WORDS = Number(process.env.REPLY_GATE_MAX_WORDS) || 300;
const DEBUG = process.env.REPLY_GATE_DEBUG === "1";
let input = "";
try {
input = readFileSync(0, "utf8");
} catch {
process.exit(0);
}
if (input.charCodeAt(0) === 0xfeff) {
input = input.slice(1);
}
let payload = {};
try {
payload = JSON.parse(input) ?? {};
} catch {
process.exit(0);
}
if (payload.stop_hook_active) {
process.exit(0);
}
let lines = [];
try {
lines = readFileSync(String(payload.transcript_path ?? ""), "utf8").split("\n");
} catch {
process.exit(0);
}
// The final reply = every text block of the newest text-bearing assistant
// message (one API message can span multiple JSONL lines, one per block).
let reply = "";
let replyId = null;
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i].trim();
if (!line) {
continue;
}
let entry;
try {
entry = JSON.parse(line);
} catch {
continue;
}
// Turn boundary (user prompt or tool result): a turn that ends with no
// text reply passes quietly instead of re-judging an older, sent reply.
if (entry?.type === "user" && !entry?.isSidechain) {
break;
}
if (entry?.type !== "assistant" || entry?.isSidechain) {
continue;
}
const content = entry?.message?.content;
if (!Array.isArray(content)) {
continue;
}
const text = content
.filter((c) => c?.type === "text" && typeof c.text === "string")
.map((c) => c.text)
.join("\n")
.trim();
if (!text) {
continue;
}
const id = entry?.message?.id ?? null;
if (replyId === null) {
replyId = id;
reply = text;
continue;
}
if (id !== null && id === replyId) {
reply = `${text}\n${reply}`;
continue;
}
break;
}
if (!reply) {
process.exit(0);
}
const prose = reply.replace(/```[\s\S]*?(?:```|$)/g, " ").replace(/`[^`\n]*`/g, " ");
const words = prose.split(/\s+/).filter((w) => /[\p{L}\p{N}]/u.test(w)).length;
const findings = [];
if (words > MAX_WORDS) {
findings.push(`${words} words of prose (ceiling ${MAX_WORDS}). Run a compression pass.`);
}
if (DEBUG) {
process.stderr.write(`reply-gate debug: ${words} words\n`);
}
if (findings.length === 0) {
process.exit(0);
}
process.stderr.write(
`Reply gate on the final reply:\n- ${findings.join("\n- ")}\nThe blocked reply is ALREADY on the user's screen; never resend it in full (that renders as a duplicate). Send only a compressed TLDR of it. If the length was genuinely required (depth the reader asked for), say so in one line and stop; this gate does not fire twice on one turn.\n`
);
process.exit(2);
#!/usr/bin/env node
// UserPromptSubmit token handoff: surfaces the PREVIOUS turn's cost inside the
// next reply, for hosts that do not render Stop-hook systemMessage output
// (some app surfaces; plain terminal sessions render it and need no handoff).
// Reads the per-session handoff file that token-report.mjs writes at stop,
// injects one instruction line via stdout at exit 0, and consumes the message
// so a zero-cost turn never re-injects a stale count (the cumulative field
// stays behind for the reporter). Routes and reports only - never blocks;
// infra errors degrade to silent pass, same as its siblings (a broken
// reporter must not tax or block prompts).
import { readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
function fmt(n) {
return Number(n || 0).toLocaleString("en-US");
}
try {
let raw = readFileSync(0, "utf8");
if (raw.charCodeAt(0) === 0xfeff) {
raw = raw.slice(1);
}
const payload = JSON.parse(raw) ?? {};
const sessionId = String(payload.session_id ?? "").replace(/[^\w-]/g, "");
if (!sessionId) {
process.exit(0);
}
const file = join(tmpdir(), `token-report-${sessionId}.json`);
const state = JSON.parse(readFileSync(file, "utf8"));
if (!state || typeof state.total !== "number") {
process.exit(0);
}
const line = `Tokens last turn: ${fmt(state.total)} (session ~${fmt(state.cum ?? state.total)})`;
delete state.total;
writeFileSync(file, JSON.stringify(state));
process.stdout.write(
`Token handoff: the previous turn's cost is below. End your current reply with this single line, verbatim, after everything else:\n${line}\n`
);
} catch {}
process.exit(0);
#!/usr/bin/env node
// Stop-hook token reporter: sums the turn's full token cost (main session plus
// sync, background, and workflow subagents) into one systemMessage line.
// Reporter, never a gate: always exits 0, silent on any internal error and on
// a zero-cost turn. Also drops the numbers to a per-session tmp handoff file
// (token-handoff.mjs surfaces them next turn on hosts that render no
// systemMessage) and appends a daily JSONL ledger line under
// ~/.claude/token-ledger/. A blocked stop re-runs Stop hooks
// (stop_hook_active), so a re-run of the SAME turn replaces its own prior
// handoff count instead of adding to it; ledger lines carry the turn-start
// timestamp, so consumers dedupe per (session, turn), last write wins.
// TOKEN_REPORT_NOW (ISO) freezes "now" for fixtures;
// TOKEN_REPORT_DEBUG=1 prints a per-source breakdown to stderr.
import { appendFileSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { basename, dirname, join } from "node:path";
const GRACE_MS = 60000;
const DEBUG = process.env.TOKEN_REPORT_DEBUG === "1";
function parseLines(text) {
const entries = [];
for (const line of text.split("\n")) {
if (!line.trim()) {
continue;
}
try {
const parsed = JSON.parse(line);
if (parsed !== null && typeof parsed === "object") {
entries.push(parsed);
}
} catch {
// The transcript is appended while we run; tolerate a truncated line.
}
}
return entries;
}
function textHead(content) {
if (typeof content === "string") {
return content;
}
if (Array.isArray(content)) {
for (const block of content) {
if (block && block.type === "text" && typeof block.text === "string") {
return block.text;
}
}
}
return "";
}
// Turn start = last real user entry: not a tool_result carrier, not meta,
// not local-command echo, not an interrupt marker.
function isTurnStart(entry) {
if (entry.type !== "user" || entry.isMeta === true) {
return false;
}
if (entry.isCompactSummary === true) {
return false;
}
const content = entry.message && entry.message.content;
if (Array.isArray(content) && content.some((b) => b && b.type === "tool_result")) {
return false;
}
const head = textHead(content);
if (head.startsWith("<local-command-stdout>") || head.startsWith("<local-command-caveat>")) {
return false;
}
return !head.startsWith("[Request interrupted by user");
}
// One API response spans many JSONL lines (one per content block); dedupe per
// message.id and keep the last snapshot, which carries the final output count.
function collectUsage(map, entry) {
if (entry.type !== "assistant") {
return;
}
const msg = entry.message;
if (!msg || !msg.usage || msg.model === "<synthetic>") {
return;
}
const key = msg.id || entry.requestId || entry.uuid;
if (!key) {
return;
}
map.set(key, { usage: msg.usage, ts: Date.parse(entry.timestamp) });
}
function sumUsage(map, startMs, endMs) {
const t = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0, calls: 0 };
for (const { usage, ts } of map.values()) {
if (startMs !== null && !Number.isNaN(ts) && (ts < startMs || ts > endMs)) {
continue;
}
t.input += usage.input_tokens || 0;
t.output += usage.output_tokens || 0;
t.cacheRead += usage.cache_read_input_tokens || 0;
t.cacheCreate += usage.cache_creation_input_tokens || 0;
t.calls += 1;
}
return t;
}
// Only agent transcripts carry usage; meta.json, journal.jsonl, and .output
// files never match this pattern, so echoes of the same cost are never reread.
function agentFiles(dir) {
try {
return readdirSync(dir)
.filter((f) => /^agent-[0-9a-f]+\.jsonl$/.test(f))
.map((f) => join(dir, f));
} catch {
return [];
}
}
function fmt(n) {
return n.toLocaleString("en-US");
}
function debugLine(label, t) {
console.error(`[token-report] ${label}: ${t.calls} calls, in ${t.input}, out ${t.output}, cache_read ${t.cacheRead}, cache_create ${t.cacheCreate}`);
}
function run() {
let stdin = readFileSync(0, "utf8");
if (stdin.charCodeAt(0) === 0xfeff) {
stdin = stdin.slice(1);
}
const hook = JSON.parse(stdin);
const transcriptPath = String(hook.transcript_path || "");
if (!transcriptPath) {
return;
}
const entries = parseLines(readFileSync(transcriptPath, "utf8"));
let startIdx = -1;
for (let i = entries.length - 1; i >= 0; i--) {
if (isTurnStart(entries[i])) {
startIdx = i;
break;
}
}
if (startIdx < 0) {
return;
}
const startMs = Date.parse(entries[startIdx].timestamp);
if (Number.isNaN(startMs)) {
return;
}
let nowMs = process.env.TOKEN_REPORT_NOW ? Date.parse(process.env.TOKEN_REPORT_NOW) : Date.now();
if (Number.isNaN(nowMs)) {
nowMs = Date.now();
}
const endMs = nowMs + GRACE_MS;
// Main transcript: positional scope, turn start to EOF. Skip inline
// sidechain entries so older builds that embed them never double count.
const mainMap = new Map();
for (let i = startIdx; i < entries.length; i++) {
if (entries[i].isSidechain === true) {
continue;
}
collectUsage(mainMap, entries[i]);
}
const main = sumUsage(mainMap, null, endMs);
if (DEBUG) {
console.error(`[token-report] turn start ${entries[startIdx].timestamp} (entry ${startIdx + 1} of ${entries.length})`);
debugLine("main", main);
}
const sessionDir = transcriptPath.endsWith(".jsonl")
? transcriptPath.slice(0, -6)
: join(dirname(transcriptPath), String(hook.session_id || ""));
const subDir = join(sessionDir, "subagents");
const candidates = agentFiles(subDir);
const wfRoot = join(subDir, "workflows");
try {
for (const name of readdirSync(wfRoot)) {
if (name.startsWith("wf_")) {
candidates.push(...agentFiles(join(wfRoot, name)));
}
}
} catch {
// No workflow runs in this session.
}
const sub = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0, calls: 0 };
let subCount = 0;
for (const file of candidates) {
let stat;
try {
stat = statSync(file);
} catch {
continue;
}
if (stat.mtimeMs < startMs - GRACE_MS) {
continue;
}
let text;
try {
text = readFileSync(file, "utf8");
} catch {
continue;
}
const map = new Map();
for (const entry of parseLines(text)) {
collectUsage(map, entry);
}
const t = sumUsage(map, startMs, endMs);
if (t.input + t.output + t.cacheRead + t.cacheCreate <= 0) {
continue;
}
subCount += 1;
sub.input += t.input;
sub.output += t.output;
sub.cacheRead += t.cacheRead;
sub.cacheCreate += t.cacheCreate;
sub.calls += t.calls;
if (DEBUG) {
debugLine(basename(file), t);
}
}
const out = main.output + sub.output;
const inp = main.input + sub.input;
const cache = main.cacheRead + main.cacheCreate + sub.cacheRead + sub.cacheCreate;
const total = out + inp + cache;
if (total <= 0) {
return;
}
let message = `Tokens this turn: ${fmt(total)} total (out ${fmt(out)}, in ${fmt(inp)}, cache ${fmt(cache)})`;
if (subCount > 0) {
const subTotal = sub.input + sub.output + sub.cacheRead + sub.cacheCreate;
message += `, incl. ${subCount} subagent${subCount === 1 ? "" : "s"}: ${fmt(subTotal)}`;
}
process.stdout.write(JSON.stringify({ systemMessage: message }) + "\n");
const sessionId = String(hook.session_id ?? "unknown").replace(/[^\w-]/g, "");
const turnIso = String(entries[startIdx].timestamp ?? "");
try {
// Handoff for the next-turn surfacing hook; cum survives the consume.
const handoffPath = join(tmpdir(), `token-report-${sessionId}.json`);
let cum = 0;
let prev = null;
try {
prev = JSON.parse(readFileSync(handoffPath, "utf8"));
cum = Number(prev.cum) || 0;
} catch {}
if (hook.stop_hook_active === true && prev && typeof prev.total === "number" && prev.turn === turnIso) {
cum -= prev.total;
}
writeFileSync(handoffPath, JSON.stringify({ total, cum: cum + total, subCount, ts: nowMs, turn: turnIso }));
} catch {}
try {
const dir = join(homedir(), ".claude", "token-ledger");
mkdirSync(dir, { recursive: true });
const iso = new Date(nowMs).toISOString();
appendFileSync(
join(dir, `${iso.slice(0, 10)}.jsonl`),
JSON.stringify({ ts: iso, turn: turnIso, session: sessionId, total, out, in: inp, cache, subagents: subCount }) + "\n"
);
} catch {}
}
try {
run();
} catch (err) {
if (DEBUG) {
console.error(`[token-report] error: ${err && err.message}`);
}
}
process.exit(0);
+74
-11
#!/usr/bin/env node
// @jayofemi/toolbox installer: copy selected skills and commands into a Claude
// Code config. Zero-dependency Node ESM, no build step.
// @jayofemi/toolbox installer: copy selected skills, commands, hooks, and
// agents into a Claude Code config. Zero-dependency Node ESM, no build step.

@@ -45,3 +45,3 @@ import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, cpSync } from "node:fs";

/** Discover the commands and skills bundled in the package. */
/** Discover the commands, skills, hooks, and agents bundled in the package. */
export function discoverEntries(root = PKG_ROOT) {

@@ -74,2 +74,27 @@ const entries = [];

}
const hooksManifest = join(root, "hooks", "manifest.json");
if (existsSync(hooksManifest)) {
for (const [name, meta] of Object.entries(JSON.parse(readFileSync(hooksManifest, "utf8")))) {
const source = join(root, "hooks", `${name}.mjs`);
if (existsSync(source)) {
entries.push({ name, type: "hook", source, description: meta.description ?? "", event: meta.event });
} else {
process.stderr.write(`hooks/manifest.json lists ${name} but hooks/${name}.mjs is missing; skipping it\n`);
}
}
}
const agentsDir = join(root, "agents");
if (existsSync(agentsDir)) {
for (const file of readdirSync(agentsDir)) {
if (file.endsWith(".md")) {
const source = join(agentsDir, file);
entries.push({
name: basename(file, ".md"),
type: "agent",
source,
description: frontmatterDescription(readFileSync(source, "utf8")),
});
}
}
}
return entries.sort((a, b) => a.name.localeCompare(b.name));

@@ -100,5 +125,12 @@ }

function targetFor(entry, base) {
return entry.type === "command"
? join(base, ".claude", "commands", `${entry.name}.md`)
: join(base, ".claude", "skills", entry.name);
if (entry.type === "command") {
return join(base, ".claude", "commands", `${entry.name}.md`);
}
if (entry.type === "hook") {
return join(base, ".claude", "hooks", `${entry.name}.mjs`);
}
if (entry.type === "agent") {
return join(base, ".claude", "agents", `${entry.name}.md`);
}
return join(base, ".claude", "skills", entry.name);
}

@@ -111,6 +143,6 @@

mkdirSync(dirname(target), { recursive: true });
if (entry.type === "command") {
if (entry.type === "skill") {
cpSync(entry.source, target, { recursive: true });
} else {
writeFileSync(target, readFileSync(entry.source, "utf8"));
} else {
cpSync(entry.source, target, { recursive: true });
}

@@ -120,5 +152,22 @@ return existed ? "updated" : "installed";

/** Settings snippet for installed hooks: scripts copy in, but they run only
* once wired into ~/.claude/settings.json; this prints the exact block. */
export function hookWiringSnippet(hookEntries, base = homedir()) {
const byEvent = {};
for (const entry of hookEntries) {
const event = entry.event || "Stop";
byEvent[event] ??= [];
const scriptPath = targetFor(entry, base).replace(/\\/g, "/");
byEvent[event].push({ type: "command", command: `node "${scriptPath}"` });
}
const hooks = {};
for (const [event, cmds] of Object.entries(byEvent)) {
hooks[event] = [{ hooks: cmds }];
}
return JSON.stringify({ hooks }, null, 2);
}
function printUsage() {
process.stdout.write(
"toolbox - install Claude Code skills and commands\n\n" +
"toolbox - install Claude Code skills, commands, hooks, and agents\n\n" +
"Usage:\n" +

@@ -128,3 +177,4 @@ " npx @jayofemi/toolbox pick from a list and install\n" +

" npx @jayofemi/toolbox add <name> install one or more by name\n\n" +
"Entries install into ~/.claude (skills/ and commands/).\n",
"Entries install into ~/.claude (skills/, commands/, hooks/, agents/).\n" +
"Hooks also need one settings.json block; the installer prints it.\n",
);

@@ -197,2 +247,15 @@ }

}
const hooksInstalled = selected.filter((e) => e.type === "hook");
if (hooksInstalled.length) {
process.stdout.write(
"\nHooks run only once wired into ~/.claude/settings.json. Merge this into its hooks block\n" +
"(hooks on one event run in parallel, so never rely on array order; new hooks apply from your next session):\n\n" +
`${hookWiringSnippet(hooksInstalled)}\n`,
);
}
if (selected.some((e) => e.type === "agent")) {
process.stdout.write(
"\nNew agents register mid-session; overrides of built-in names (Explore, Plan) apply from your next session start.\n",
);
}
return 0;

@@ -199,0 +262,0 @@ }

+5
-3
{
"name": "@jayofemi/toolbox",
"version": "1.6.0",
"description": "An open library of reusable Claude Code skills, commands, and scripts, with a one-command installer.",
"version": "1.7.0",
"description": "An open library of reusable Claude Code skills, commands, hooks, and agents.",
"type": "module",

@@ -12,3 +12,5 @@ "bin": {

"commands/",
"skills/"
"skills/",
"hooks/",
"agents/"
],

@@ -15,0 +17,0 @@ "scripts": {

# Toolbox
Small, portable tools for Claude Code: skills, commands, and scripts pulled from real daily use, packaged so you can install one in seconds and own it.
Small, portable tools for Claude Code: skills, commands, hooks, and agents pulled from real daily use, packaged so you can install one in seconds and own it.

@@ -23,6 +23,8 @@ Each entry is small and does one thing. They install by copying into your Claude Code config, carry no lock-in, and work with whatever model you run. Read them, change them, keep what is useful.

Two kinds of entry, split by who invokes them:
Four kinds of entry, split by how they run:
- **Commands** are slash commands you invoke by name, like `/screenshot`. They install to `~/.claude/commands/`.
- **Skills** are ones Claude reaches for on its own when a task matches their description, and you can invoke them by name too. They install to `~/.claude/skills/`.
- **Hooks** are scripts your Claude Code config runs mechanically at fixed moments (a reply finishing, a prompt arriving). They install to `~/.claude/hooks/`, and the installer prints the one settings block that wires them; new hooks apply from your next session.
- **Agents** are subagent definitions with a pinned model tier. They install to `~/.claude/agents/`; new agents register mid-session, and overrides of built-in names (Explore, Plan) apply from your next session start.

@@ -41,5 +43,24 @@ ### Commands

### Hooks
- **[token-report](hooks/token-report.mjs)**: see what every turn cost. Totals each turn's token spend, subagents included, prints it as a system line, and appends a daily ledger under `~/.claude/token-ledger/`. A reporter, never a gate.
- **[token-handoff](hooks/token-handoff.mjs)**: the same count, surfaced inside the next reply, for apps that do not render system lines. Pairs with token-report.
- **[reply-gate](hooks/reply-gate.mjs)**: keep replies readable. Bounces a final reply over the prose-length ceiling once, with the exact word count, then always passes the revision. Default 300 words; `REPLY_GATE_MAX_WORDS` overrides.
### Agents (the model-routing switch)
Four subagent seats, dynamic by default: nothing ever runs above your session's own tier, **Plan** and **Review** ride the session tier exactly (the bookends get the strongest model you are already paying for), **Explore** stays on cheap Haiku for sweeps, and **Execute** runs Sonnet capped at the session tier for spec-driven edits. The **[model-routing](commands/model-routing.md)** command reports the seats, pins any of them to a fixed model (a pin is honored verbatim), or frees them back to dynamic. Install the four agents and the command together; the switch needs the installer route, because plugin-scoped agents cannot take the built-in Explore and Plan names.
Unless you pin a seat, the resolution per session model:
| Session model | Explore | Plan | Execute | Review |
|---|---|---|---|---|
| Haiku | haiku | haiku | haiku | haiku |
| Sonnet | haiku | sonnet | sonnet | sonnet |
| Opus | haiku | opus | sonnet | opus |
| Your strongest tier | haiku | that tier | sonnet | that tier |
## Manual install
Prefer to copy by hand? A command is `commands/<name>.md` into `~/.claude/commands/`; a skill is the `skills/<name>/` folder into `~/.claude/skills/`.
Prefer to copy by hand? A command is `commands/<name>.md` into `~/.claude/commands/`; a skill is the `skills/<name>/` folder into `~/.claude/skills/`; a hook is `hooks/<name>.mjs` into `~/.claude/hooks/` plus its settings block (event per `hooks/manifest.json`); an agent is `agents/<Name>.md` into `~/.claude/agents/`.

@@ -50,3 +71,3 @@ ## Other ways to install

- **Claude Code plugin**: run `/plugin marketplace add JayOfemi/claude-toolbox`, then `/plugin install toolbox@jayofemi`. Plugin installs namespace the entries, so the commands become `/toolbox:screenshot` and so on.
- **Claude Code plugin**: run `/plugin marketplace add JayOfemi/claude-toolbox`, then `/plugin install toolbox@jayofemi`. Plugin installs namespace the entries, so the commands become `/toolbox:screenshot` and so on. The plugin also wires the two token hooks automatically (no settings edit; run `/reload-plugins` after a plugin update to pick up hook changes; if you previously wired those hooks through the installer, remove that settings block so they run once, not twice). The reply gate stays opt-in through the installer, and the model-routing agents need the installer route (plugin agents cannot take the built-in seat names; the plugin may list namespaced copies like `toolbox:Explore`, which the switch ignores since it manages only `~/.claude/agents/`). The same marketplace also lists two MCP server plugins, [shikamaru](https://github.com/JayOfemi/shikamaru) (finance math) and [ai-checker](https://github.com/JayOfemi/byakugan) (AI-text analysis), installable with `/plugin install shikamaru@jayofemi` and `/plugin install ai-checker@jayofemi`.
- **The skills CLI**: `npx skills add JayOfemi/claude-toolbox` installs the skills (not the commands) for Claude Code and other agents that read `SKILL.md` folders.

@@ -53,0 +74,0 @@