SLOPSHOPPER

calc

/calc evaluates arithmetic with parentheses and decimals, and says what is wrong with a bad expression

newcommand
v0.1.0MITupdated 2026-10-14MatheusChiodi/cockpit/mods/calc
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · calc
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /calc ⎿ calc: Usage: /calc <expression> e.g. /calc (2 + 3) * 4 ^ 2 / 8 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Cockpit

English · Português · Español

An instrument panel for Claude Code. 309 mods for the terminal: 52 panels that stay in sight and switch on and off in /cockpit, plus guards, prompt automations, instant commands and focus helpers. Every mod is a small plugin you install with one line.

The full Cockpit panel in a terminal

/plugin marketplace add MatheusChiodi/cockpit
/plugin install git-band@cockpit
/cockpit

Mods are TypeScript functions that run inside Claude Code and change how it behaves: they intercept tool calls, rewrite prompts, draw panes and bands, and register slash commands. They need Claude Code 2.1.287 or newer (built against 2.1.296).

Install

From GitHub, inside Claude Code:

/plugin marketplace add MatheusChiodi/cockpit
/plugin install <mod>@cockpit

From a local folder:

git clone https://github.com/MatheusChiodi/cockpit
claude plugin marketplace add ./cockpit
claude plugin install git-guard@cockpit

Just to try one, without installing:

claude --plugin-dir ./cockpit/mods/turn-timer

Remove a mod with /plugin uninstall <mod>@cockpit. Turn every mod off at once with claude --safe-mode.

Panels and settings

The 52 mods under Always visible are panels: bands above the prompt or status line entries that update on their own (git, context, cost, tests, CI, tasks, deadline, weather). None needs a command to show up.

The cockpit mod is their settings pane, and it comes along as a dependency of any panel.

ToDo
Open the settings pane/cockpit — Tab and arrows move, Enter or a click toggles, Esc closes
Switch a panel off or on/cockpit off git-band · /cockpit on git-band
Switch all of them/cockpit off all · /cockpit on all
Change the language/cockpit lang en · /cockpit lang pt · /cockpit lang es

Both choices are kept between sessions. A band disappears or returns at once; a status line entry disappears at once and returns on its next update. A panel switched off stays installed and simply does not draw.

Bands stack in the order the mods load, and the status line holds one entry per mod. With many panels on, the terminal gets crowded: start with four or five and adjust in /cockpit.

Language

Every mod speaks English by default and can switch to Brazilian Portuguese or Spanish. The language is set once, in /cockpit, and every installed mod follows it: band labels, status line words, command output, guard messages, toasts and notifications. Without the cockpit mod installed, mods stay in English.

Slash command descriptions in the typeahead are read when a session starts, so they follow a language change on the next session. To make Claude itself answer in the chosen language, install reply-language.

Catalog

Always visible53cockpit git-band todo-band usage-band files-band error-band goal-band agents-band activity-status test-status ci-status stash-status uncommitted-band pr-status main-warning-band conflict-band project-status ports-band deps-status todo-count-status quality-status model-status cost-ticker idle-clock turn-progress last-prompt-band compact-nudge session-diff-band shield-band reset-countdown spend-today focus-band deadline-band pin-band clock-status day-timer commits-today dolar-status weather-status api-status terminal-pet runtime-band docker-band env-drift-band deploy-status todo-new-status release-status license-status install-status db-port-status migration-status env-ignore-status build-size-status
Security42rm-guard env-guard git-guard secret-scan prompt-secret-guard lockfile-guard path-jail main-branch-guard generated-guard sql-guard prod-guard install-ask pipe-shell-guard big-read-guard sudo-guard chmod-guard docker-guard kill-guard credential-file-guard upload-guard env-dump-guard registry-guard persistence-guard dotfile-guard ci-config-guard test-skip-guard suppress-guard migration-guard large-write-guard tunnel-guard hardcoded-secret-guard private-host-guard dynamic-exec-guard sql-concat-guard tls-off-guard cors-open-guard debug-flag-guard script-mode-guard dep-source-guard weak-crypto-guard gitignore-guard archive-add-guard
Interface34turn-timer tool-tally file-trail cmd-log git-status-line context-meter rate-limit-watch session-clock diff-pane turn-history prompt-history agent-pane token-chart cost-pane error-pane context-pane read-pane links-pane git-pane branch-pane stash-pane code-todo-pane deps-pane scripts-pane env-keys-pane test-pane slow-toast deny-toast big-output-toast timeline tool-clock result-fold group-peek prompt-stamp
Prompt and model30concise-mode persona prompt-shortcuts tldr git-context stack-context commit-style reply-language time-context os-context test-first explain-mode plan-first no-comments minimal-diff ask-first verify-claims paste-fence file-mention-hint trace-helper branch-naming pr-style a11y-rules security-rules react-rules tailwind-rules rn-rules java-rules oracle-rules answer-format
Commands42quick-notes pomodoro copy-last session-stats quote-selection git-peek git-branch git-stash git-undo run-script open-path ports kill-port tree find-file last-error env-check outdated json-tool uuid timestamp base64 hash calc color-tool regex-test http-probe snippets export-session standup loc-count big-files recent-files symbol-def file-author head-file file-diff path-size ignore-check file-compare churn hit-count
Focus and notifications42turn-notify break-reminder daily-streak error-notify needs-you-notify agent-done-notify long-cmd-notify tests-notify ci-notify pr-review-notify rate-limit-notify hydrate posture eye-rest stand-up alarms remind timebox session-cap late-night intent-check daily-target weekly-recap breathe budget-alert daily-budget-alert turn-toast milestone-toast push-reminder idle-commit-nudge topic-switch scatter-toast long-turn-toast step-back branch-drift edits-no-commit day-close quiet-window parking-lot file-churn resume-line wandering
Git and GitHub24commit-review wip-commit-guard marker-guard amend-guard stage-guard git-stage git-fixup git-sync behind-toast git-switch git-tag staged-status pr-open pr-checks pr-comments review-queue issue-context issue-link draft-nag pr-diff merge-ready release-notes changelog-entry merged-branches
Quality12untested-turn-toast red-turn-toast debugger-guard leftovers-toast any-guard long-file-toast dup-block-guard untested coverage-status empty-test-guard snapshot-update-guard catch-swallow-guard
Stacks12pm-guard script-guard missing-import-guard client-env-guard next-boundary-guard esm-guard alias-import-guard dev-server-guard ts-config-context env-undeclared-toast node-version-status pm-command
Context and cost18reread-guard grep-limit read-cap git-log-limit paste-size-toast cat-guard explore-nudge fill-toast cache-cold-toast flood-note heavy-calls tree-flood-guard agent-cap agent-straggler cost-per-hour cost-trend priciest-turn runout-projection

Visual language

Every panel is drawn the same way, so several of them read as one instrument:

  • Instrument row: a colored rail, a fixed-width label, then values. Stacked, the bands line up in columns.
  • One hue per subject on the rail: Git, Session, Project and Daily. Green, amber and red are reserved for the state of a value.
  • Thin-line gauges (━━━━───) instead of block bars.
  • A short, lowercase status line: label state detail, as in tests ✓ 14:32 · 3.1s.
  • Colors come from your Claude Code theme. The drawing lives in kit/kit.tsx, copied into each mod at build time.

Suggested sets

ForMods
An everyday dashboardgit-band usage-band todo-band error-band test-status
Git without surprisesgit-band conflict-band uncommitted-band main-warning-band pr-status ci-status
An eye on spendusage-band cost-ticker spend-today reset-countdown compact-nudge
A seat beltrm-guard git-guard env-guard secret-scan pipe-shell-guard prod-guard shield-band
Less typingstack-context git-context commit-style prompt-shortcuts git-peek
Focusfocus-band deadline-band pin-band turn-notify break-reminder

Before you install

  • Mods are not sandboxed. They run with the same reach as Claude Code. Read a mod's hooks/register.ts before installing it; every one here is short.
  • Guards are a seat belt, not a vault. They match known shapes of commands and paths: an obfuscated command can get through, and a legitimate path can be refused. Keep Claude Code's own permissions configured.
  • Some guards ask instead of blocking. prod-guard, install-ask and the others that say so force the confirmation prompt even when the command was already allowed.
  • A few mods call a model and cost tokens; their description says so.
  • Some panels use the network and stay hidden when there is no answer: dolar-status (AwesomeAPI), weather-status (wttr.in, located by IP), api-status (Anthropic's status page), ports-band (localhost only).
  • ci-status and pr-status need an authenticated gh.
  • Git panels poll the repository every 30 to 60 seconds.
  • No sound. In this version $.audio.play only plays on macOS, so notifications use the native channel.
  • The mods API is early access and may change between Claude Code releases.

Development

npm install
npm run build              # manifests, the hub's panel list, previews and the three READMEs
npm run check              # tsc + claude plugin validate on every mod + claude plugin test where tests exist
node scripts/mod.mjs <mod> # the same loop for one mod, without touching shared files

mods.json is the single source: name, category, descriptions in three languages and the preview lines of every mod.

mods/<name>/
  hooks/register.ts(x)   the mod
  hooks/strings.ts       every user-facing text, in en, pt and es
  types/index.d.ts       the $.state contract, for mods that draw
  tests/                 run by `claude plugin test`

plugin.json, hooks.json and each mod's copy of the kit are generated.

Adding mods with the agent. The repository ships a Claude Code agent, cockpit-mod-builder (.claude/agents/), that knows the conventions: the language pattern, the visual kit, the engine rules that trip people up, and the one-mod loop. Open Claude Code in the repository and ask for it by name, giving a category and the mods you want.

The type check uses the API declarations Claude Code itself writes. They are not in the repository: run a mod with claude --plugin-dir mods/<name> and copy mods/<name>/.claude-plugin/types/claude-code/index.d.ts plus claude-code-tools/index.d.ts into engine-types/.

The mods ecosystem

Surveyed on 2026-10-13 through gh api and the official documentation. Star counts are from that day.

Community collectionStarsKnown for
karanb192/awesome-claude-code-mods560A catalog of 3,836 mods across 1,987 repositories
YeonwooSung/my-claude-code-mods514agent-profiler, context-inspector, time-machine
hamzafer/claude-code-mods227context-bar, blast-radius, merge-gate
zenbu-labs/terminal-browser3,854A browser inside the terminal
jarrodwatts/claude-image-view222Images in the terminal

Other ways people customize Claude Code, which are not mods: claude-hud and ccstatusline (status line scripts), ccusage (a CLI over the transcripts), tweakcc (patches the install), cc-safety-net (a PreToolUse hook).

License

MIT

Source 2 files
hooks/register.ts 204 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3import { STRINGS } from './strings'
4
5// cockpit: the language set in /cockpit (English when the hub is not installed)
6const LANG = { plugin: 'cockpit', key: 'lang' } as const
7
8const strings = async ($: EngineInterface) => STRINGS[(await $.state.get(LANG)).value ?? 'en']
9
10// The typed text, else the mouse selection, else ''.
11const input = async ($: EngineInterface, args: string): Promise<string> => {
12  if (args.trim() !== '') {
13    return args.trim()
14  }
15
16  try {
17    return ((await $.ui.selection())?.text ?? '').trim()
18  } catch {
19    return ''
20  }
21}
22
23type Token = { text: string; at: number; value?: number }
24type Failure = { kind: 'character' | 'unexpected' | 'end' | 'paren' | 'zero' | 'invalid'; found: string; at: number }
25
26const fail = (kind: Failure['kind'], found = '', at = 0): never => {
27  throw { kind, found, at } satisfies Failure
28}
29
30// Numbers, operators and parentheses; `at` is the 1-based position in the text.
31const tokenize = (text: string): Token[] => {
32  const tokens: Token[] = []
33  const number = /\d+\.?\d*|\.\d+/y
34  let at = 0
35
36  while (at < text.length) {
37    const char = text.charAt(at)
38    number.lastIndex = at
39    const hit = number.exec(text)
40
41    if (/\s/.test(char)) {
42      at += 1
43    } else if (hit !== null) {
44      tokens.push({ text: hit[0], at: at + 1, value: Number(hit[0]) })
45      at = number.lastIndex
46    } else if ('+-*/%^()'.includes(char)) {
47      tokens.push({ text: char, at: at + 1 })
48      at += 1
49    } else {
50      fail('character', char, at + 1)
51    }
52  }
53
54  return tokens
55}
56
57// Recursive descent: expr > term > unary > power > primary. `^` binds tighter than a unary minus and is right-associative.
58const evaluate = (text: string): number => {
59  const tokens = tokenize(text)
60  let at = 0
61  const isOp = (value: string) => tokens[at]?.value === undefined && tokens[at]?.text === value
62  const take = (value: string) => {
63    if (!isOp(value)) {
64      return false
65    }
66
67    at += 1
68
69    return true
70  }
71  const primary = (): number => {
72    const token = tokens[at]
73
74    if (token === undefined) {
75      return fail('end')
76    }
77
78    if (token.value !== undefined) {
79      at += 1
80
81      return token.value
82    }
83
84    if (token.text !== '(') {
85      return fail('unexpected', token.text, token.at)
86    }
87
88    at += 1
89    const inner = expr()
90
91    if (!take(')')) {
92      const next = tokens[at]
93
94      return next === undefined ? fail('paren', '(', token.at) : fail('unexpected', next.text, next.at)
95    }
96
97    return inner
98  }
99  const power = (): number => {
100    const base = primary()
101
102    return take('^') ? base ** unary() : base
103  }
104  const unary = (): number => {
105    if (take('-')) {
106      return -unary()
107    }
108
109    return take('+') ? unary() : power()
110  }
111  const term = (): number => {
112    let value = unary()
113
114    for (;;) {
115      const op = ['*', '/', '%'].find(take)
116
117      if (op === undefined) {
118        return value
119      }
120
121      const right = unary()
122
123      if (op !== '*' && right === 0) {
124        fail('zero')
125      }
126
127      value = op === '*' ? value * right : op === '/' ? value / right : value % right
128    }
129  }
130  const expr = (): number => {
131    let value = term()
132
133    for (;;) {
134      if (take('+')) {
135        value += term()
136      } else if (take('-')) {
137        value -= term()
138      } else {
139        return value
140      }
141    }
142  }
143  const value = expr()
144  const left = tokens[at]
145
146  if (left !== undefined) {
147    fail('unexpected', left.text, left.at)
148  }
149
150  return Number.isFinite(value) ? value : fail('invalid')
151}
152
153// The result as text, or the message for what is wrong with the expression.
154const calculate = (text: string, s: (typeof STRINGS)['en']): { result?: string; error?: string } => {
155  try {
156    return { result: String(Number(evaluate(text).toPrecision(12))) }
157  } catch (thrown) {
158    const failure = thrown as Failure
159
160    switch (failure.kind) {
161      case 'character':
162        return { error: s.character(failure.found, failure.at) }
163      case 'unexpected':
164        return { error: s.unexpected(failure.found, failure.at) }
165      case 'paren':
166        return { error: s.paren(failure.at) }
167      case 'zero':
168        return { error: s.zero }
169      case 'invalid':
170        return { error: s.invalid }
171      default:
172        return { error: s.end }
173    }
174  }
175}
176
177export const register: Register = on => {
178  on('session.start', async ($, e, next) => {
179    const s = await strings($)
180    await $.command.register({ name: 'calc', description: s.command, argumentHint: s.hint })
181
182    return next(e)
183  })
184
185  on('command.run', { command: 'calc' }, async ($, e) => {
186    const s = await strings($)
187    const text = await input($, e.args)
188
189    if (text === '') {
190      return { text: s.usage }
191    }
192
193    const { result, error } = calculate(text, s)
194
195    if (result === undefined) {
196      return { text: error }
197    }
198
199    await $.ui.copy({ text: result })
200
201    return { text: `${text} = ${result}\n${s.copied}` }
202  })
203}
204
hooks/strings.ts 43 lines
1const en = {
2  command: 'Calculate an expression with + - * / % ^, parentheses and decimals; uses the mouse selection when none is given',
3  hint: '<expression>',
4  usage: 'Usage: /calc <expression>  e.g. /calc (2 + 3) * 4 ^ 2 / 8',
5  copied: 'Result copied to the clipboard.',
6  character: (found: string, at: number) => `Unexpected character "${found}" at position ${at}.`,
7  unexpected: (found: string, at: number) => `Unexpected "${found}" at position ${at}.`,
8  end: 'The expression ends too soon: something is missing after the last operator.',
9  paren: (at: number) => `The "(" at position ${at} is never closed.`,
10  zero: 'Division by zero.',
11  invalid: 'The result is not a finite number.',
12}
13
14type Strings = typeof en
15
16const pt: Strings = {
17  command: 'Calcula uma expressão com + - * / % ^, parênteses e decimais; usa a seleção do mouse quando não há texto',
18  hint: '<expressão>',
19  usage: 'Uso: /calc <expressão>  ex.: /calc (2 + 3) * 4 ^ 2 / 8',
20  copied: 'Resultado copiado para a área de transferência.',
21  character: (found, at) => `Caractere inesperado "${found}" na posição ${at}.`,
22  unexpected: (found, at) => `"${found}" inesperado na posição ${at}.`,
23  end: 'A expressão termina cedo demais: falta algo depois do último operador.',
24  paren: at => `O "(" da posição ${at} nunca é fechado.`,
25  zero: 'Divisão por zero.',
26  invalid: 'O resultado não é um número finito.',
27}
28
29const es: Strings = {
30  command: 'Calcula una expresión con + - * / % ^, paréntesis y decimales; usa la selección del ratón cuando no hay texto',
31  hint: '<expresión>',
32  usage: 'Uso: /calc <expresión>  p. ej.: /calc (2 + 3) * 4 ^ 2 / 8',
33  copied: 'Resultado copiado al portapapeles.',
34  character: (found, at) => `Carácter inesperado "${found}" en la posición ${at}.`,
35  unexpected: (found, at) => `"${found}" inesperado en la posición ${at}.`,
36  end: 'La expresión termina demasiado pronto: falta algo después del último operador.',
37  paren: at => `El "(" de la posición ${at} nunca se cierra.`,
38  zero: 'División por cero.',
39  invalid: 'El resultado no es un número finito.',
40}
41
42export const STRINGS = { en, pt, es }
43