SLOPSHOPPER

ClauJev mod

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

newcommandtoaststatus
★ 4v2.1.0MITupdated 2026-10-20Civalski/claujev/plugins/claujev-mod
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · claujev-mod
› 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 › /claujev ⎿ claujev-mod: No Jev activity logged today. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ claujev-mod: Jev ✓0 ⚑0
README

ClauJev mod

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.

  • Automatic effort: reads the effort Jev suggested for the current request and, with 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.
  • Status line: Jev ✓42 ⚑3 · effort high — checks and actions of the day, and the effort.
  • Toasts when Jev acts (command held, final check blocked).
  • /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

Source 1 files
hooks/register.ts 218 lines
1// 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