Jauge du contexte et du quota, au-dessus de la zone de saisie

Cinq mods pour Claude Code, utilisables dans n'importe quel projet. Un mod est un petit ajout qui change ce que Claude Code affiche ou ce qu'il accepte de faire.
| Mod | Ce qu'il fait |
|---|---|
| jauge | Une barre au-dessus de la zone de saisie : remplissage du fil (en %) et quota sur 5 heures. Prévient à 85 %. Dans un projet Claude (application Projets), ajoute aussi une petite ligne « 📊 Jauge » en bas de chaque message des fils (remplissage, quotas 5 h et 7 jours, durée et actions du tour, durée de la session), puisque la barre n'y est pas visible. |
| chrono | Dans la ligne d'état : durée réelle du dernier tour et nombre d'appels d'outils, lus sur l'horloge. |
| sonnette | Une notification quand Claude a fini un travail de plus d'une minute. |
| garde-fou | Refuse les commandes qu'on ne peut pas défaire (changer l'adresse distante git, pousser en force ou sur main, supprimer en masse, effacer des changements non sauvegardés) et dit pourquoi. |
| bilan | La commande /bilan : coût, durée, remplissage du fil et quotas de la session. |
Tous les chiffres sont lus sur Claude Code lui-même, jamais estimés.
Dans Claude Code, à l'invite, une ligne par mod voulu :
/plugin install jauge --marketplace graphixxgeek-boop/CCM-claude-code-mods
/plugin install chrono --marketplace graphixxgeek-boop/CCM-claude-code-mods
/plugin install sonnette --marketplace graphixxgeek-boop/CCM-claude-code-mods
/plugin install garde-fou --marketplace graphixxgeek-boop/CCM-claude-code-mods
/plugin install bilan --marketplace graphixxgeek-boop/CCM-claude-code-mods
Répondre y pour ajouter la source, puis choisir la portée.
Depuis un terminal : claude plugin install <mod> --marketplace graphixxgeek-boop/CCM-claude-code-mods.
Dans l'application de bureau : cliquer sur + à côté de la zone de saisie, puis Plugins, Add marketplace avec graphixxgeek-boop/CCM-claude-code-mods, puis Add plugin.
Le dépôt est privé : pour installer, GitHub doit être configuré sur l'ordinateur.
/plugin liste les mods chargés et permet de les éteindre.
Il lit le texte entier de la commande. Une commande qui contient seulement le texte d'une commande interdite (par exemple dans un fichier qu'on écrit) est refusée aussi. C'est voulu : mieux vaut un refus de trop qu'une commande irréversible qui passe.
claude plugin validate <dossier du mod>
claude plugin test garde-fouhooks/register.tsx 131 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Mesure } from '../types'
5
6const mesure = atom({ plugin: 'jauge', key: 'mesure' } as const, null)
7const cachee = atom({ plugin: 'jauge', key: 'cachee' } as const, false)
8
9const SEUIL_ALERTE = 85
10const CASES = 10
11// Les outils dont le texte arrive tel quel à la personne, dans son fil.
12const OUTILS_DE_REPONSE = ['mcp__hearthbot__post_message', 'mcp__hearthbot__reply']
13
14// Relit la vraie mesure du moteur, jamais une estimation (heures et chiffres lus, jamais tapés).
15async function mesurer($: EngineInterface) {
16 const { context, rateLimits } = await $.session.usage()
17 const percent = context.percent ?? null
18 const cinqHeures = rateLimits.find(r => r.kind === 'five_hour')
19 const avant = await read($, mesure)
20 const alerte = percent !== null && percent >= SEUIL_ALERTE
21 const m: Mesure = { percent, quota: cinqHeures?.percentUsed ?? null, alerte }
22 await update($, mesure, () => m)
23 if (alerte && !avant?.alerte) {
24 $.ui.toast(`Jauge : le fil est plein à ${percent} %. Pensez à résumer ou à ouvrir un fil neuf.`)
25 }
26}
27
28// Claude Code donne les quotas avec une décimale au plus, souvent aucune : on montre celle qu'il donne, jamais un « ,0 » ajouté.
29const uneDecimale = (n: number) => (Number.isInteger(n) ? String(n) : n.toFixed(1).replace('.', ','))
30
31// Un nombre de tokens en clair : 850, 12 k, 3,4 M.
32function tokensEnClair(n: number) {
33 if (n < 1000) return String(n)
34 if (n < 1_000_000) return `${Math.round(n / 1000)} k`
35 return `${(n / 1_000_000).toFixed(1).replace('.', ',')} M`
36}
37
38// Une durée = l'écart de deux heures LUES sur l'horloge du moteur, jamais une impression.
39function enClair(ms: number) {
40 const s = Math.round(ms / 1000)
41 if (s < 60) return `${s} s`
42 if (s < 3600) return `${Math.floor(s / 60)} min ${String(s % 60).padStart(2, '0')} s`
43 return `${Math.floor(s / 3600)} h ${String(Math.floor((s % 3600) / 60)).padStart(2, '0')}`
44}
45
46export const register: Register = on => {
47 let debutDuTour = 0
48 let actions = 0
49 // Tokens réellement traités, tels que l'API les compte à chaque requête (entrée, sortie, cache).
50 let tokensTour = 0
51 let tokensSession = 0
52
53 on('prompt.submit', async ($, e, next) => {
54 debutDuTour = await $.clock.now()
55 actions = 0
56 tokensTour = 0
57 return next(e)
58 }).catch(($, e, next) => next(e))
59
60 on('turn.step', async function* ($, e, next) {
61 const r = yield* next(e)
62 const u = r?.usage
63 if (u) {
64 const n = u.input_tokens + u.output_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens
65 tokensTour += n
66 tokensSession += n
67 }
68 return r
69 })
70
71 on('turn.complete', async ($, e, next) => {
72 const ran = await next(e)
73 await mesurer($)
74 return ran
75 })
76
77 on('tool.call', async ($, e, next) => {
78 if (!e.tool.startsWith('mcp__hearthbot__')) actions += 1
79 const ran = await next(e)
80 await mesurer($)
81 return ran
82 }).catch(($, e, next) => next(e))
83
84 // Dans l'app Projets, la personne ne voit pas la barre : seuls les messages des fils lui
85 // parviennent. La jauge y ajoute donc une ligne, lue gratuitement, en bas de chaque message.
86 // L'outil de réponse a changé de nom (reply, puis post_message) : on accroche tous les noms
87 // connus, sinon la ligne disparaît sans bruit au prochain renommage.
88 for (const outil of OUTILS_DE_REPONSE) on('tool.call', { tool: outil }, async ($, e, next) => {
89 await mesurer($)
90 const m = await read($, mesure)
91 const texte = (e as { text?: unknown }).text
92 if (typeof texte !== 'string' || texte.includes('🚨 _fil rempli')) {
93 return next(e)
94 }
95 const fil = m?.percent != null ? `${m.percent} %` : 'pas encore mesuré'
96 const quota = m?.quota != null ? ` · quota 5 h : ${uneDecimale(m.quota)} %` : ''
97 const maintenant = await $.clock.now()
98 const { startedAt, rateLimits } = await $.session.usage()
99 const septJours = rateLimits.find(r => r.kind === 'seven_day')
100 const semaine = septJours ? ` · quota 7 j : ${uneDecimale(septJours.percentUsed)} %` : ''
101 const tour = debutDuTour > 0 ? ` · ce tour : ${enClair(maintenant - debutDuTour)}, ${actions} action${actions > 1 ? 's' : ''}` : ''
102 const tokens = tokensSession > 0 ? ` · tokens : ce tour ${tokensEnClair(tokensTour)}, session ${tokensEnClair(tokensSession)}` : ''
103 const session = ` · session : ${enClair(maintenant - startedAt)}${tokens}`
104 const alerte = m?.alerte ? ' · ⚠️ pensez à un fil neuf' : ''
105 return next({ ...e, text: `${texte}\n\n🚨 _fil rempli à ${fil}${quota}${semaine}${tour}${session}${alerte}_` })
106 }).catch(($, e, next) => next(e))
107
108 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
109 const m = await read($, mesure)
110 if (e.props.hasSurvey || m === null || m.percent === null || (await read($, cachee))) {
111 return next(e)
112 }
113
114 const { Box, Button, Text } = $.ui.resolve(e)
115 const pleines = Math.round((m.percent / 100) * CASES)
116 const barre = '█'.repeat(pleines) + '░'.repeat(CASES - pleines)
117 const couleur = m.percent >= SEUIL_ALERTE ? 'red' : m.percent >= 60 ? 'yellow' : 'green'
118
119 return (
120 <Box>
121 <Text>Jauge </Text>
122 <Text color={couleur}>{barre}</Text>
123 <Text> {m.percent} % du fil</Text>
124 {m.quota !== null && <Text dimColor> · quota 5 h : {m.quota} %</Text>}
125 <Text> </Text>
126 <Button key="cacher" label="Cacher" onPress={() => update($, cachee, () => true)} />
127 </Box>
128 )
129 })
130}
131types/index.d.ts 8 lines1export type Mesure = { percent: number | null; quota: number | null; alerte: boolean }
2
3declare module 'claude-code' {
4 interface PluginState {
5 jauge: { mesure: Mesure | null; cachee: boolean }
6 }
7}
8