Optional companion of ClauJev / Complemento opcional do ClauJev: automatic effort, status line, toasts and /claujev.

Install the core first, so Claude Code asks for your TypeSafe AI key: /plugin install claujev --marketplace Civalski/claujev, then /plugin install claujev-mod --marketplace Civalski/claujev.
Optional companion of the claujev plugin (installed with it as a dependency). It needs a Claude Code version with mods.
effort: on, applies it to each model request of the main loop, within minEffort … maxEffort. It never touches subagents or a request that asks for effort itself ("ultrathink"). Default: observe, which only shows the suggestion.Jev ✓42 ⚑3 · effort high — checks and actions of the day, and the effort./claujev: today's calls, actions, cost and effort suggestions.Settings: /claujev:configure (saved in ~/.claude/claujev/config.json under mod). Full documentation: https://github.com/Civalski/claujev
hooks/register.ts 218 lines1// ClauJev mod. Copyright (c) 2026 Alisson Civalski — MIT License — https://github.com/Civalski/claujev
2import type { EngineInterface, Register } from 'claude-code'
3
4// The claujev plugin's hooks write to ~/.claude/claujev; the mod only reads it.
5// Triage (UserPromptSubmit) stores the effort suggestion in state/<session>.json,
6// and every hook appends a line to logs/<day>.jsonl.
7
8type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max'
9const LEVELS: readonly Effort[] = ['low', 'medium', 'high', 'xhigh', 'max']
10
11type TurnState = { effort?: Effort | null; explicitEffort?: boolean }
12type LogEntry = {
13 hook?: string
14 acted?: boolean
15 error?: string
16 effort?: string
17 flagged?: string[]
18 problems?: string[]
19 inputTokens?: number
20}
21
22const PRICE_PER_M_INPUT = 0.042
23
24const TEXT = {
25 en: {
26 effort: 'effort',
27 suggested: 'suggested',
28 acted: (hook: string, detail: string) => `Jev acted (${hook}): ${detail}`,
29 summaryTitle: 'ClauJev today',
30 noLogs: 'No Jev activity logged today.',
31 calls: 'calls',
32 actedCol: 'acted',
33 errors: 'errors',
34 cost: 'cost',
35 efforts: 'suggested effort',
36 commandDesc: "Show today's Jev activity: calls, actions, cost and suggested effort",
37 },
38 pt: {
39 effort: 'esforço',
40 suggested: 'sugerido',
41 acted: (hook: string, detail: string) => `Jev agiu (${hook}): ${detail}`,
42 summaryTitle: 'ClauJev hoje',
43 noLogs: 'Nenhuma atividade do Jev registrada hoje.',
44 calls: 'chamadas',
45 actedCol: 'agiu',
46 errors: 'erros',
47 cost: 'custo',
48 efforts: 'esforço sugerido',
49 commandDesc: 'Mostra a atividade do Jev hoje: chamadas, ações, custo e esforço sugerido',
50 },
51}
52type Texts = (typeof TEXT)['en'] | (typeof TEXT)['pt']
53
54// What the hooks share; module variables start over on a reload, which only costs one re-read.
55type Ctx = { t: Texts; showStatus: boolean; seenLines: number; mode: string; minEffort: Effort; maxEffort: Effort }
56
57// Preferences written by /claujev:configure in ~/.claude/claujev/config.json:
58// { "lang": "pt", "mod": { "effort": "observe" | "on" | "off", "minEffort": "low", "maxEffort": "xhigh", "status": true } }
59type Settings = { lang?: string; mod?: { effort?: string; minEffort?: string; maxEffort?: string; status?: boolean } }
60
61const isLevel = (v: unknown): v is Effort => typeof v === 'string' && (LEVELS as readonly string[]).includes(v)
62
63// Keeps the suggestion inside the limits the user set.
64export function clampEffort(suggested: Effort, min: Effort, max: Effort): Effort {
65 const i = LEVELS.indexOf(suggested)
66 const lo = LEVELS.indexOf(min)
67 const hi = Math.max(lo, LEVELS.indexOf(max))
68 return LEVELS[Math.min(Math.max(i, lo), hi)] ?? suggested
69}
70
71export function parseLog(text: string): LogEntry[] {
72 const out: LogEntry[] = []
73 for (const line of text.split('\n')) {
74 if (!line.trim()) continue
75 try {
76 out.push(JSON.parse(line) as LogEntry)
77 } catch {
78 // a line cut while being written; the next read gets it whole
79 }
80 }
81 return out
82}
83
84export function effortLabel(t: Texts, d: { effort: Effort; applied: boolean } | null | undefined): string | undefined {
85 return d ? `${t.effort} ${d.effort}${d.applied ? '' : ` (${t.suggested})`}` : undefined
86}
87
88async function dataDir($: EngineInterface): Promise<string | undefined> {
89 const custom = await $.env.get('CLAUJEV_HOME')
90 if (custom) return custom.replace(/\\/g, '/')
91 const home = (await $.env.get('HOME')) || (await $.env.get('USERPROFILE'))
92 return home ? `${home.replace(/\\/g, '/')}/.claude/claujev` : undefined
93}
94
95// Re-read at session start and at each new request, so a change made by /claujev:configure applies right away.
96async function loadSettings($: EngineInterface, ctx: Ctx): Promise<void> {
97 const dir = await dataDir($)
98 let s: Settings = {}
99 try {
100 if (dir) s = JSON.parse(await $.fs.read(`${dir}/config.json`)) as Settings
101 } catch {
102 // no config yet: defaults
103 }
104 const m = s.mod ?? {}
105 ctx.t = s.lang === 'pt' ? TEXT.pt : TEXT.en
106 ctx.mode = m.effort === 'on' || m.effort === 'off' ? m.effort : 'observe'
107 ctx.minEffort = isLevel(m.minEffort) ? m.minEffort : 'low'
108 ctx.maxEffort = isLevel(m.maxEffort) ? m.maxEffort : 'xhigh'
109 ctx.showStatus = m.status !== false
110}
111
112async function readState($: EngineInterface): Promise<TurnState | null> {
113 const dir = await dataDir($)
114 if (!dir) return null
115 const sid = (await $.session.id()).replace(/[^\w-]/g, '')
116 try {
117 return JSON.parse(await $.fs.read(`${dir}/state/${sid}.json`)) as TurnState
118 } catch {
119 return null
120 }
121}
122
123async function todayLog($: EngineInterface): Promise<LogEntry[]> {
124 const dir = await dataDir($)
125 if (!dir) return []
126 const day = new Date(await $.clock.now()).toISOString().slice(0, 10)
127 try {
128 return parseLog(await $.fs.read(`${dir}/logs/${day}.jsonl`))
129 } catch {
130 return []
131 }
132}
133
134// Shows the day's numbers, and a toast for each new action Jev took since the last look.
135async function refresh($: EngineInterface, ctx: Ctx, label?: string): Promise<void> {
136 const entries = await todayLog($)
137 if (ctx.seenLines >= 0) {
138 for (const e of entries.slice(ctx.seenLines)) {
139 if (!e.acted || e.hook === 'prompt') continue
140 const detail = e.problems?.[0] || e.flagged?.[0] || ''
141 $.ui.toast(ctx.t.acted(e.hook ?? '?', detail.slice(0, 120)))
142 }
143 }
144 ctx.seenLines = entries.length
145 if (!ctx.showStatus) return
146 const calls = entries.filter(e => !e.error).length
147 const acted = entries.filter(e => e.acted).length
148 $.ui.status([`Jev ✓${calls} ⚑${acted}`, label].filter(Boolean).join(' · '))
149}
150
151async function summary($: EngineInterface, t: Texts): Promise<string> {
152 const entries = await todayLog($)
153 if (!entries.length) return t.noLogs
154 const byHook = new Map<string, { calls: number; acted: number; errors: number }>()
155 const efforts = new Map<string, number>()
156 let tokens = 0
157 for (const e of entries) {
158 const hook = e.hook ?? '?'
159 const s = byHook.get(hook) ?? { calls: 0, acted: 0, errors: 0 }
160 s.calls += 1
161 if (e.acted) s.acted += 1
162 if (e.error) s.errors += 1
163 byHook.set(hook, s)
164 tokens += e.inputTokens ?? 0
165 if (hook === 'prompt' && e.effort) efforts.set(e.effort, (efforts.get(e.effort) ?? 0) + 1)
166 }
167 const rows = [...byHook].map(([hook, s]) => ` ${hook.padEnd(9)} ${t.calls} ${s.calls} ${t.actedCol} ${s.acted} ${t.errors} ${s.errors}`)
168 const effortLine = LEVELS.filter(l => efforts.has(l)).map(l => `${l} ${efforts.get(l)}`).join(' · ')
169 return [
170 t.summaryTitle,
171 ...rows,
172 ` ${t.cost} US$ ${((tokens / 1e6) * PRICE_PER_M_INPUT).toFixed(4)}`,
173 effortLine ? ` ${t.efforts}: ${effortLine}` : '',
174 ].filter(Boolean).join('\n')
175}
176
177export const register: Register = on => {
178 const ctx: Ctx = { t: TEXT.en, showStatus: true, seenLines: -1, mode: 'observe', minEffort: 'low', maxEffort: 'xhigh' }
179 const decided = new Map<string, { effort: Effort; applied: boolean } | null>()
180
181 on('session.start', async ($, e, next) => {
182 await loadSettings($, ctx)
183 await $.command.register({ name: 'claujev', description: ctx.t.commandDesc })
184 void refresh($, ctx)
185 return next(e)
186 })
187
188 // Effort is set per model request, only in the main loop, never in subagents.
189 // turn.step streams: the hook is an async generator and `yield* next(...)` forwards the stream.
190 on('turn.step', async function* ($, e, next) {
191 if (e.agentId !== undefined || e.effort === undefined) return yield* next(e)
192
193 if (!decided.has(e.turnId)) {
194 await loadSettings($, ctx)
195 if (ctx.mode === 'off') {
196 decided.set(e.turnId, null)
197 return yield* next(e)
198 }
199 const st = await readState($)
200 // A request that asks for effort itself ("ultrathink") is never overridden.
201 const suggestion = st && !st.explicitEffort && isLevel(st.effort) ? st.effort : null
202 decided.set(e.turnId, suggestion ? { effort: clampEffort(suggestion, ctx.minEffort, ctx.maxEffort), applied: ctx.mode === 'on' } : null)
203 if (decided.size > 50) decided.delete(decided.keys().next().value as string)
204 void refresh($, ctx, effortLabel(ctx.t, decided.get(e.turnId)))
205 }
206
207 const d = decided.get(e.turnId)
208 return yield* next(d?.applied ? { ...e, effort: d.effort } : e)
209 })
210
211 on('turn.complete', async ($, e, next) => {
212 void refresh($, ctx, effortLabel(ctx.t, decided.get(e.turnId)))
213 return next(e)
214 })
215
216 on('command.run', { command: 'claujev' }, async $ => ({ text: await summary($, ctx.t) }))
217}
218