SLOPSHOPPER

aiblueprint

AIBlueprint usage band above the prompt: session cost, 5-hour and weekly limits with pacing and reset time.

newbandtoaststatusprocesstimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · aiblueprint
› 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 S ██░░░ 97k/200k $0.42 5h ██░░░ 31% +49.0% 59m ⚙ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
S ██░░░ 97k/200k $0.42 5h ██░░░ 31% +49.0% 59m ⚙
README

AIBlueprint CLI

Supercharge Claude Code and Codex with security hooks, reusable skills, a lightweight statusline, and workflow automation.

📚 Full Documentation | 🎯 Premium Features

🚀 Quick Start

# Run setup (no installation required)
npx aiblueprint-cli@latest agents setup

✨ Features

  • 🛡️ Security - Command validation hooks blocking dangerous operations
  • 📊 Statusline - Git status and session context display
  • 🤖 Skills - 30+ reusable workflow skills
  • 🎭 Agents - 3 specialized AI agents for codebase exploration
  • ⚡ Scripts - Built-in statusline utilities
  • 🧩 Mods - Claude Code function-hook mods (usage band, skills sync)

📦 Installation

Option 1: Plugin (Recommended)

/plugin marketplace add melvynx/aiblueprint
/plugin install aibp-base@AIBlueprint

Option 2: CLI Tool

# Run without installation
npx aiblueprint-cli@latest agents setup

🎯 Usage

Setup

# Interactive setup
npx aiblueprint-cli@latest agents setup

# Install all features (no prompts)
npx aiblueprint-cli@latest agents setup --skip

# Custom location
npx aiblueprint-cli@latest agents setup --folder ~/.my-claude

Statusline

Install a lightweight Claude Code statusline:

npx aiblueprint-cli@latest agents setup

Other Commands

# Add specific hooks
npx aiblueprint-cli@latest agents add hook post-edit-typescript

# Create symlinks between tools
npx aiblueprint-cli@latest agents symlink

# Centralize global skills and agents in ~/.agents
npx aiblueprint-cli@latest agents unify

# Choose what to centralize interactively
npx aiblueprint-cli@latest agents unify -i

# Unify project-local .claude/.cursor config into .agents
npx aiblueprint-cli@latest agents unify projects

# Unify only selected project categories
npx aiblueprint-cli@latest agents unify projects --agents-md --skills --rules

# Recover sessions from saved configs and backups
npx aiblueprint-cli@latest agents config unify sessions

📚 What You Get

Security Hooks

  • Command validation - Blocks rm -rf, privilege escalation, remote execution
  • TypeScript processing - Auto-format and lint after file edits
  • Security logging - Tracks all blocked commands to ~/.claude/security.log

Custom Statusline

  • Git branch, changes, and repository info
  • Session context and duration
  • Colored visual indicators

Workflow Skills

The free configuration ships the skills listed in the catalog below. The CLI installs every directory under agents-config/skills/.

Agents (3 Specialized)

  • explore-codebase - Code discovery and analysis
  • Snipper - Fast code modifications
  • websearch - Quick web research

Mods

Installed into ~/.claude/mods/ and enabled through CLAUDE_CODE_PLUGIN_DIRS:

  • aiblueprint - Usage band above the prompt: session context and cost, 5-hour and weekly limits with pace and reset time
  • agent4everything - Links every .agents/skills skill (global and per project) into .claude/skills

Shell Shortcuts

  • cc - Claude Code with permissions skipped
  • ccc - Claude Code with continue mode
  • cx - Codex
  • cxc - Codex continue mode for the current directory

Skills

Install skills directly into ~/.claude/skills/:

# Install all skills
npx skills@latest add melvynx/aiblueprint

# Install a single skill
npx skills add Melvynx/aiblueprint --skill app-icon

# Install multiple skills
npx skills add Melvynx/aiblueprint --skill rules-manager
npx skills add Melvynx/aiblueprint --skill skill-manager

Available skills:

SkillDescription
agents-managerCreate and orchestrate Claude Code agents
apexStructured implementation workflow
app-iconGenerate and prepare app icons
appstore-connectManage App Store Connect workflows
audit-memoriesManually audit and clean project agent documentation
audit-skillsManually audit skill usage, invocation controls, duplicates, and scope
grill-meStress-test a plan with focused questions
hooks-managerCreate and debug Claude Code hooks
environments-managerSet up per-worktree agent environments
rules-managerCreate and maintain agent rule files
skill-managerManage skills and rules across Claude Code, Codex, and Cursor
test-auditGate new tests and prune low-value, implementation-coupled ones
use-styleApply named UI style guides before implementation

🤖 Assistant Pro

Install the latest complete skill bundle from the private assistant-pro-skills repository:

npx aiblueprint-cli@latest assistants pro setup

The command asks for the Assistant Pro access key when needed, installs the versioned all bundle into ~/.agents/skills, creates Claude Code and Codex skill symlinks, adds the shared directory to Hermes, and exposes the same skills to OpenClaw through its native ~/.agents/skills discovery.

💎 Agents Config Pro

Unlock advanced features at mlv.sh/claude-cli

# Setup premium configs (prompts for your token if needed)
npx aiblueprint-cli@latest agents pro setup

# Or activate first if you prefer
npx aiblueprint-cli@latest agents pro activate YOUR_TOKEN

🛠️ Development

# Clone and setup
git clone <repository>
cd aiblueprint-cli
bun install

# Development mode
bun run dev agents setup
bun run dev agents statusline --list

# Run tests
bun run test:run

# Build and test locally
bun run build
bun run test-local

📋 Requirements

  • Node.js 16+ or Bun
  • Claude Code installed
  • Optional: bun, gh CLI
  • Python 3 for assistants pro setup

🤝 Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Run tests: bun run test:run
  5. Submit a pull request

📄 License

MIT License - see LICENSE file for details.


Created by AIBlueprint - mlv.sh/claude-cli

Need help? Open an issue | Documentation

Source 2 files
hooks/register.tsx 310 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionUsage } from 'claude-code'
3
4import type { Limit, Usage } from '../types'
5
6const SESSION_MS = 5 * 3600 * 1000
7const WEEK_MS = 7 * 24 * 3600 * 1000
8let limitsCache: Promise<string | undefined> | undefined
9// The status line script's cache under $HOME/.claude; undefined when HOME cannot be read.
10const limitsCachePath = ($: EngineInterface) =>
11  (limitsCache ??= $.process
12    .run(['printenv', 'HOME'])
13    .then(({ stdout }) => stdout.trim() ? `${stdout.trim()}/.claude/scripts/statusline/data/usage-limits-cache.json` : undefined)
14    .catch(() => undefined))
15
16const GREEN = '#57ab5a'
17const YELLOW = '#d4a72c'
18const ORANGE = '#ff8700'
19const RED = '#e5534b'
20const TRACK = '#8b8b8b'
21const BLUE = '#539bf5'
22const BAR_WIDTH = 36
23
24const usageAtom = atom({ plugin: 'aiblueprint', key: 'usage' } as const, null)
25const settingsOpenAtom = atom({ plugin: 'aiblueprint', key: 'settingsOpen' } as const, false)
26
27type Options = {
28  showSession: boolean
29  showFiveHour: boolean
30  showWeek: boolean
31  showBars: boolean
32  showPace: boolean
33  showReset: boolean
34  sessionLabel: string
35  fiveHourLabel: string
36  weekLabel: string
37  refreshSeconds: number
38}
39
40const DEFAULTS: Options = {
41  showSession: true,
42  showFiveHour: true,
43  showWeek: true,
44  showBars: true,
45  showPace: true,
46  showReset: true,
47  sessionLabel: 'S',
48  fiveHourLabel: '5h',
49  weekLabel: 'Week',
50  refreshSeconds: 30,
51}
52
53// The toggles the ⚙ row shows, in order.
54const TOGGLES = [
55  { field: 'showSession', label: 'Session' },
56  { field: 'showFiveHour', label: '5h' },
57  { field: 'showWeek', label: 'Week' },
58  { field: 'showBars', label: 'Barres' },
59  { field: 'showPace', label: 'Pace' },
60  { field: 'showReset', label: 'Reset' },
61] as const
62
63let isRunning = false
64let isPending = false
65
66function fillColor(percent: number) {
67  if (percent < 50) return GREEN
68  if (percent < 75) return YELLOW
69  if (percent < 90) return ORANGE
70  return RED
71}
72
73// Pace > 0: under the week's pace (budget left), < 0: burning faster than time passes.
74function paceColor(pace: number) {
75  if (pace >= 0) return GREEN
76  if (pace > -10) return YELLOW
77  return RED
78}
79
80function formatPace(pace: number) {
81  return `${pace >= 0 ? '+' : ''}${pace.toFixed(1)}%`
82}
83
84function formatCost(cost: number) {
85  return `$${cost < 10 ? cost.toFixed(2) : cost.toFixed(1)}`
86}
87
88// One unit only, the biggest that fits: 6j, 13h, 45m.
89function formatResetsIn(ms: number) {
90  const minutes = Math.max(0, Math.floor(ms / 60_000))
91  if (minutes < 60) return `${minutes}m`
92  const hours = Math.floor(minutes / 60)
93  return hours < 24 ? `${hours}h` : `${Math.floor(hours / 24)}j`
94}
95
96function barSvg(percent: number, color: string) {
97  const filled = Math.max(0, Math.min(BAR_WIDTH, Math.round((percent / 100) * BAR_WIDTH)))
98  return `<svg xmlns="http://www.w3.org/2000/svg" width="${BAR_WIDTH}" height="4" viewBox="0 0 ${BAR_WIDTH} 4"><rect width="${BAR_WIDTH}" height="4" rx="2" fill="${TRACK}" fill-opacity="0.25"/><rect width="${filled}" height="4" rx="2" fill="${color}"/></svg>`
99}
100
101function barText(percent: number) {
102  const filled = Math.max(0, Math.min(5, Math.round((percent / 100) * 5)))
103  return `${'█'.repeat(filled)}${'░'.repeat(5 - filled)}`
104}
105
106// The same windows as the Plan usage limits panel: Session = the 5-hour limit, Week = the 7-day limit.
107function limitOf(usage: SessionUsage, kind: string, windowMs: number, now: number): Limit | undefined {
108  const limit = usage.rateLimits.find(item => item.kind === kind)
109  if (!limit) return undefined
110  const resetsInMs = limit.resetsAt ? new Date(limit.resetsAt).getTime() - now : undefined
111  const elapsedPercent = resetsInMs !== undefined ? ((windowMs - resetsInMs) / windowMs) * 100 : undefined
112  return {
113    percent: limit.percentUsed,
114    pace: elapsedPercent !== undefined ? elapsedPercent - limit.percentUsed : undefined,
115    resetsInMs,
116  }
117}
118
119function contextOf(usage: SessionUsage): Usage['context'] {
120  const { tokens, window, percent } = usage.context
121  const value = percent ?? (tokens !== undefined && window > 0 ? (tokens / window) * 100 : undefined)
122  return value === undefined ? undefined : { percent: value, tokens, window }
123}
124
125function formatTokens(count: number) {
126  return count >= 1000 ? `${Math.round(count / 1000)}k` : `${count}`
127}
128
129function formatContext(context: NonNullable<Usage['context']>) {
130  const tokens = context.tokens ?? Math.round((context.percent / 100) * context.window)
131  return `${formatTokens(tokens)}/${formatTokens(context.window)}`
132}
133
134// A pill with the token count written inside, like the context window bar. A darker fill and a
135// shadowed label keep the text readable over both the filled part and the empty track.
136const PILL_WIDTH = 84
137const PILL_HEIGHT = 16
138
139function contextSvg(context: NonNullable<Usage['context']>) {
140  const filled = Math.max(0, Math.min(PILL_WIDTH, Math.round((context.percent / 100) * PILL_WIDTH)))
141  return `<svg xmlns="http://www.w3.org/2000/svg" width="${PILL_WIDTH}" height="${PILL_HEIGHT}" viewBox="0 0 ${PILL_WIDTH} ${PILL_HEIGHT}"><defs><clipPath id="p"><rect width="${PILL_WIDTH}" height="${PILL_HEIGHT}" rx="${PILL_HEIGHT / 2}"/></clipPath></defs><g clip-path="url(#p)"><rect width="${PILL_WIDTH}" height="${PILL_HEIGHT}" fill="#3a3f47"/><rect width="${filled}" height="${PILL_HEIGHT}" fill="#2f6fd1"/></g><text x="${PILL_WIDTH / 2}" y="11.5" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, system-ui, sans-serif" font-size="10.5" font-weight="700" fill="#ffffff" stroke="#000000" stroke-opacity="0.35" stroke-width="2" paint-order="stroke">${formatContext(context)}</text></svg>`
142}
143
144function compute(usage: SessionUsage, now: number): Usage {
145  return {
146    cost: usage.cost?.usd ?? 0,
147    context: contextOf(usage),
148    session: limitOf(usage, 'five_hour', SESSION_MS, now),
149    week: limitOf(usage, 'seven_day', WEEK_MS, now),
150  }
151}
152
153// Keeps the terminal status line script on the engine's own readings, so it never calls the usage API.
154async function seedLimitsCache($: EngineInterface, usage: SessionUsage, now: number) {
155  const window = (kind: string) => {
156    const limit = usage.rateLimits.find(item => item.kind === kind)
157    return limit ? { utilization: limit.percentUsed, resets_at: limit.resetsAt ?? null } : null
158  }
159  const sevenDay = window('seven_day')
160  if (!sevenDay) return
161  const path = await limitsCachePath($)
162  if (!path) return
163  await $.fs
164    .write(path, JSON.stringify({ data: { five_hour: window('five_hour'), seven_day: sevenDay }, timestamp: now }, null, 2))
165    .catch(() => undefined)
166}
167
168async function refresh($: EngineInterface): Promise<void> {
169  if (isRunning) {
170    isPending = true
171    return
172  }
173  isRunning = true
174  try {
175    const [usage, now] = await Promise.all([$.session.usage(), $.clock.now()])
176    const next = compute(usage, now)
177    await update($, usageAtom, () => next)
178    await seedLimitsCache($, usage, now)
179  } catch {
180    // keep the last band
181  } finally {
182    isRunning = false
183  }
184  if (isPending) {
185    isPending = false
186    await refresh($)
187  }
188}
189
190// Writes one userConfig field the way /config does; the module reloads with the new options.
191async function setOption($: EngineInterface, field: string, value: boolean) {
192  const rows = await $.config.list()
193  const row = rows.find(item => item.key.startsWith('aiblueprint') && item.key.endsWith(`.${field}`))
194  if (!row) {
195    $.ui.toast(`aiblueprint: réglage ${field} introuvable dans /config`)
196    return
197  }
198  const { deny } = await $.config.set({ key: row.key, value })
199  if (deny) $.ui.toast(`aiblueprint: ${deny}`)
200}
201
202async function toggleSettings($: EngineInterface) {
203  await update($, settingsOpenAtom, isOpen => !isOpen)
204}
205
206export const register: Register = (on, rawOptions) => {
207  const options: Options = { ...DEFAULTS, ...(rawOptions as Partial<Options>) }
208  const refreshMs = Math.max(5, options.refreshSeconds) * 1000
209
210  on('session.start', async ($, e, next) => {
211    const started = await next(e)
212    $.ui.status(undefined)
213    void refresh($)
214    $.clock.every(refreshMs, () => refresh($))
215    return started
216  })
217
218  on('session.measure', async ($, e, next) => {
219    const measured = await next(e)
220    void refresh($)
221    return measured
222  })
223
224  on('turn.complete', async ($, e, next) => {
225    const completed = await next(e)
226    void refresh($)
227    return completed
228  })
229
230  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
231    const [usage, isSettingsOpen] = await Promise.all([read($, usageAtom), read($, settingsOpenAtom)])
232    if (e.props.hasSurvey || usage === null) return next(e)
233
234    const limits = [
235      { key: 'five-hour', label: options.fiveHourLabel, limit: options.showFiveHour ? usage.session : undefined },
236      { key: 'week', label: options.weekLabel, limit: options.showWeek ? usage.week : undefined },
237    ].filter((item): item is { key: string; label: string; limit: Limit } => item.limit !== undefined)
238
239    const { Box, Text, Button } = $.ui.resolve(e)
240    const settingsRow = isSettingsOpen && (
241      <Box key="settings" flexDirection="row" gap={1} flexWrap="wrap">
242        {TOGGLES.map(({ field, label }) => (
243          <Button
244            key={`toggle-${field}`}
245            label={`${options[field] ? '✓' : '·'} ${label}`}
246            dimColor={!options[field]}
247            onPress={() => setOption($, field, !options[field])}
248          />
249        ))}
250      </Box>
251    )
252    const gear = <Button key="gear" label="⚙" plain dimColor={!isSettingsOpen} onPress={() => toggleSettings($)} />
253
254    if (e.surface === 'terminal') {
255      return (
256        <Box flexDirection="column">
257          <Box flexDirection="row" gap={3}>
258            {options.showSession && (
259              <Text key="session">
260                <Text dimColor>{options.sessionLabel} </Text>
261                {usage.context !== undefined && <Text color={BLUE}>{options.showBars ? `${barText(usage.context.percent)} ` : ''}{formatContext(usage.context)} </Text>}
262                <Text bold>{formatCost(usage.cost)}</Text>
263              </Text>
264            )}
265            {limits.map(({ key, label, limit }) => (
266              <Text key={key}>
267                <Text dimColor>{label} </Text>
268                {options.showBars && <Text color={fillColor(limit.percent)}>{barText(limit.percent)} </Text>}
269                <Text bold>{Math.round(limit.percent)}%</Text>
270                {options.showPace && limit.pace !== undefined && <Text color={paceColor(limit.pace)}> {formatPace(limit.pace)}</Text>}
271                {options.showReset && limit.resetsInMs !== undefined && <Text dimColor> {formatResetsIn(limit.resetsInMs)}</Text>}
272              </Text>
273            ))}
274            {gear}
275          </Box>
276          {settingsRow}
277        </Box>
278      )
279    }
280
281    const { Svg } = $.ui.resolve(e)
282    return (
283      <Box flexDirection="column" gap={1}>
284        <Box flexDirection="row" gap={1} flexWrap="wrap" alignItems="center">
285          {options.showSession && (
286            <Box key="session" flexDirection="row" alignItems="center" gap={1} borderStyle="round" borderDimColor paddingX={1}>
287              <Text dimColor>{options.sessionLabel}</Text>
288              {usage.context !== undefined && (
289                <Svg source={contextSvg(usage.context)} alt={`Contexte ${formatContext(usage.context)}`} width={PILL_WIDTH} height={PILL_HEIGHT} />
290              )}
291              <Text bold>{formatCost(usage.cost)}</Text>
292            </Box>
293          )}
294          {limits.map(({ key, label, limit }) => (
295            <Box key={key} flexDirection="row" alignItems="center" gap={1} borderStyle="round" borderDimColor paddingX={1}>
296              <Text dimColor>{label}</Text>
297              {options.showBars && <Svg source={barSvg(limit.percent, fillColor(limit.percent))} alt={`${label} ${Math.round(limit.percent)}%`} width={BAR_WIDTH} height={4} />}
298              <Text bold>{Math.round(limit.percent)}%</Text>
299              {options.showPace && limit.pace !== undefined && <Text bold color={paceColor(limit.pace)}>{formatPace(limit.pace)}</Text>}
300              {options.showReset && limit.resetsInMs !== undefined && <Text dimColor>{formatResetsIn(limit.resetsInMs)}</Text>}
301            </Box>
302          ))}
303          {gear}
304        </Box>
305        {settingsRow}
306      </Box>
307    )
308  })
309}
310
types/index.d.ts 15 lines
1export type Limit = { percent: number; pace?: number; resetsInMs?: number }
2
3export type Usage = {
4  cost: number
5  context?: { percent: number; tokens?: number; window: number }
6  session?: Limit
7  week?: Limit
8}
9
10declare module 'claude-code' {
11  interface PluginState {
12    aiblueprint: { usage: Usage | null; settingsOpen: boolean }
13  }
14}
15