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

Supercharge Claude Code and Codex with security hooks, reusable skills, a lightweight statusline, and workflow automation.
📚 Full Documentation | 🎯 Premium Features
# Run setup (no installation required)
npx aiblueprint-cli@latest agents setup
/plugin marketplace add melvynx/aiblueprint
/plugin install aibp-base@AIBlueprint
# Run without installation
npx aiblueprint-cli@latest agents 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
Install a lightweight Claude Code statusline:
npx aiblueprint-cli@latest agents setup
# 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
rm -rf, privilege escalation, remote execution~/.claude/security.logThe free configuration ships the skills listed in the catalog below. The CLI installs every directory under agents-config/skills/.
Installed into ~/.claude/mods/ and enabled through CLAUDE_CODE_PLUGIN_DIRS:
.agents/skills skill (global and per project) into .claude/skillscc - Claude Code with permissions skippedccc - Claude Code with continue modecx - Codexcxc - Codex continue mode for the current directoryInstall 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:
| Skill | Description |
|---|---|
agents-manager | Create and orchestrate Claude Code agents |
apex | Structured implementation workflow |
app-icon | Generate and prepare app icons |
appstore-connect | Manage App Store Connect workflows |
audit-memories | Manually audit and clean project agent documentation |
audit-skills | Manually audit skill usage, invocation controls, duplicates, and scope |
grill-me | Stress-test a plan with focused questions |
hooks-manager | Create and debug Claude Code hooks |
environments-manager | Set up per-worktree agent environments |
rules-manager | Create and maintain agent rule files |
skill-manager | Manage skills and rules across Claude Code, Codex, and Cursor |
test-audit | Gate new tests and prune low-value, implementation-coupled ones |
use-style | Apply named UI style guides before implementation |
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.
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
# 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
bun, gh CLIassistants pro setupbun run test:runMIT License - see LICENSE file for details.
Created by AIBlueprint - mlv.sh/claude-cli
Need help? Open an issue | Documentation
hooks/register.tsx 310 lines1import { 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}
310types/index.d.ts 15 lines1export 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