Цель с прогресс-баром: /goal-meter <цель> разбивает на шаги и отмечает готовые

Пять модов - маленьких плагинов, которые меняют интерфейс Claude Code: показывают кэш и лимиты, прячут личное на записи экрана, рисуют прогресс цели и не дают двум чатам править один файл вслепую.
Из ролика на канале @amir_aktay.
В Claude Code:
/plugin marketplace add pewdredd/claude-code-mods
/plugin install cache-keeper@aktay-mods
Остальные ставятся так же, меняется только имя: collision-guard, goal-meter, next-steps, recording-mode. Можно поставить любые по отдельности.
| Мод | Что делает | Как пользоваться |
|---|---|---|
| cache-keeper | Над полем ввода: сколько осталось жить кэшу промпта (60 минут), заполнение контекста, лимиты 5 часов и недели, цена сессии. За 5 минут до остывания кэша - уведомление. Кнопка «перенести сессию»: Claude пишет сводку сессии, потом кнопка «Очистить и продолжить» сама делает /clear и отправляет сводку в чистый чат. Пока включён режим записи, здесь же горит красный «● REC» | работает сам |
| next-steps | После ответа - три кнопки «что дальше», одним нажатием отправляют следующий запрос | работает сам, /next-steps - вкл/выкл |
| recording-mode | Пока пишешь экран, прячет в транскрипте почты, ключи, телефоны и суммы. Claude при этом видит всё как есть. Метку «● REC» рисует cache-keeper - ставь их вместе | /record on, /record off |
| goal-meter | Разбивает цель на шаги, отдаёт агенту и показывает полоску прогресса. После каждого ответа отмечает готовые шаги | /goal-meter <цель>, /goal-meter off |
| collision-guard | Если файл в последние 30 минут правил другой чат Claude Code - останавливает правку и просит агента спросить тебя. Повторная правка проходит | работает сам |
CACHE_TTL_MS в cache-keeper/hooks/register.tsx.Опиши словами, что мод должен показывать и когда, и попроси Claude Code его собрать. Любой мод отсюда - хороший образец: три файла в папке плагина и один register.tsx.
hooks/register.tsx 113 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { GoalMeterGoal } from '../types'
5
6// Goal Meter: /goal-meter <цель> — цель разбивается на шаги, над полем ввода
7// рисуется прогресс-бар, после каждого ответа модель отмечает готовые шаги.
8const goal = atom({ plugin: 'goal-meter', key: 'goal' } as const, null)
9
10function jsonArray(text: string): unknown[] {
11 const match = text.match(/\[[\s\S]*\]/)
12 if (!match) return []
13 try {
14 const list = JSON.parse(match[0])
15 return Array.isArray(list) ? list : []
16 } catch {
17 return []
18 }
19}
20
21const BAR_WIDTH = 24
22
23export const register: Register = on => {
24 on('session.start', async ($, e, next) => {
25 await $.command.register({
26 name: 'goal-meter',
27 description: 'Поставить цель с прогресс-баром (/goal-meter off — убрать)',
28 argumentHint: '<цель> | off',
29 })
30 return next(e)
31 })
32
33 on('command.run', { command: 'goal-meter' }, async ($, e) => {
34 const text = e.args.trim()
35 if (text === '' || text === 'off') {
36 await update($, goal, () => null)
37 return { text: 'Цель снята.' }
38 }
39
40 const split = await $.model.complete({
41 model: 'claude-haiku-4-5-20251001',
42 prompt:
43 `Break this goal into 3-7 concrete, checkable steps. Answer in the goal's language. ` +
44 `Reply with a JSON array of short strings and nothing else.\n\nGoal: ${text}`,
45 maxTokens: 500,
46 })
47 const steps = split.isAnswered
48 ? jsonArray(split.text).filter((s): s is string => typeof s === 'string').slice(0, 7)
49 : []
50 const value: GoalMeterGoal = { text, steps: steps.map(s => ({ text: s, isDone: false })) }
51 await update($, goal, () => value)
52
53 const plan = value.steps.map((s, i) => `${i + 1}. ${s.text}`).join('\n')
54 $.clock.after(0, () => void $.prompt.submit({ text: `Цель: ${text}\n\nШаги:\n${plan}\n\nВыполни их по порядку.` }))
55 return { text: `Цель поставлена, шагов: ${value.steps.length}.` }
56 })
57
58 on('turn.complete', async ($, e, next) => {
59 const result = await next(e)
60 const current = await read($, goal)
61 if (e.agentId || e.reason !== 'answer' || current === null || current.steps.length === 0) return result
62
63 $.clock.after(0, async () => {
64 const list = current.steps.map((s, i) => `${i}. ${s.text}`).join('\n')
65 const answer = await $.model.fork({
66 prompt:
67 `Which of these steps are fully done in this conversation so far?\n${list}\n\n` +
68 `Reply with a JSON array of the done step numbers and nothing else.`,
69 })
70 if (!answer.isAnswered) return
71 const done = new Set(jsonArray(answer.text).filter((n): n is number => typeof n === 'number'))
72 await update($, goal, g =>
73 g === null ? g : { ...g, steps: g.steps.map((s, i) => ({ ...s, isDone: s.isDone || done.has(i) })) },
74 )
75 })
76 return result
77 })
78
79 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
80 const current = await read($, goal)
81 if (e.props.hasSurvey || current === null) return next(e)
82
83 const { Box, Text } = $.ui.resolve(e)
84 const done = current.steps.filter(s => s.isDone).length
85 const total = current.steps.length
86 const next_ = current.steps.find(s => !s.isDone)
87
88 const filled = total === 0 ? 0 : Math.round((done / total) * BAR_WIDTH)
89 const mine = (
90 <Box flexDirection="column" marginTop={1}>
91 <Text>
92 <Text dimColor>цель: </Text>
93 <Text bold>{current.text}</Text>
94 </Text>
95 <Text>
96 <Text color="success">{'━'.repeat(filled)}</Text>
97 <Text dimColor>{'━'.repeat(BAR_WIDTH - filled)}</Text>
98 <Text color={done === total ? 'success' : undefined}>{` ${done}/${total}`}</Text>
99 </Text>
100 {next_ && <Text dimColor>{`сейчас: ${next_.text}`}</Text>}
101 </Box>
102 )
103
104 // место над полем ввода общее: дорисовываем под собой строки других модов
105 return (
106 <Box flexDirection="column">
107 {mine}
108 {await next(e)}
109 </Box>
110 )
111 })
112}
113types/index.d.ts 11 lines1export type GoalMeterGoal = {
2 text: string
3 steps: Array<{ text: string; isDone: boolean }>
4}
5
6declare module 'claude-code' {
7 interface PluginState {
8 'goal-meter': { goal: GoalMeterGoal | null }
9 }
10}
11