SLOPSHOPPER

ctx

Agents, skiils and plagins for Claude

newspinnerstatustimer
v?NOASSERTIONupdated 2026-10-10GafarovMaxim/Claude/mods/ctx
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · ctx
› 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 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › CTX ▪▪▪▪▫▫▫▫ 49% 5H ▪▪▫▫▫▫▫▫ 31% ◷ 60м

Draws

Prompt hint
CTX ▪▪▪▪▫▫▫▫ 49% 5H ▪▪▫▫▫▫▫▫ 31% ◷ 60м
README

max-marketplace

Личный маркетплейс плагинов Максима для Claude. Сейчас в нём один плагин: max-kit.

  • plugins/max-kit/: агенты, скиллы и правило пула (описание в plugins/max-kit/README.md)
  • Подключение: claude.ai → Customize → Plugins → Add → Add marketplace → GafarovMaxim/Claude, включить Sync automatically
  • Новый агент: файл в plugins/max-kit/agents/, поднять version в plugins/max-kit/.claude-plugin/plugin.json, закоммитить в main
Source 2 files
hooks/register.tsx 106 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Gauge } from '../types'
5
6// Шкалы под полем ввода: контекст (подпись — имя мода «ctx»), 5H — лимит пяти часов, 7D — недели,
7// ◷ — минут до остывания кэша. Десктоп под вводом рисует только строку статуса — туда шкалы текстом;
8// терминал рисует строку подсказки — туда шкалы цветом. Передача в чистый контекст — отдельный мод handoff.
9const gauge = atom({ plugin: 'ctx', key: 'gauge' } as const, { ctx: null, h5: null, d7: null, cacheMin: null })
10const lastTurnAt = atom({ plugin: 'ctx', key: 'lastTurnAt' } as const, 0)
11const isTerminal = atom({ plugin: 'ctx', key: 'isTerminal' } as const, false)
12
13const CACHE_TTL_MIN = 60 // кэш промптов на подписке живёт час с последнего запроса (у API бывает 5 мин)
14const CELLS = 8
15
16let timer: (() => void) | undefined
17
18function cells(p: number, n = CELLS): string {
19  const full = Math.max(0, Math.min(n, Math.round((p / 100) * n)))
20  return '▪'.repeat(full) + '▫'.repeat(n - full) // ▪ и ▫ — малые квадраты одной пары, одного размера
21}
22
23function line(g: Gauge): string {
24  const parts: string[] = []
25  if (g.ctx !== null) parts.push(`${cells(g.ctx)} ${Math.round(g.ctx)}%`)
26  if (g.h5 !== null) parts.push(`5H ${cells(g.h5)} ${Math.round(g.h5)}%`)
27  if (g.d7 !== null) parts.push(`7D ${cells(g.d7)} ${Math.round(g.d7)}%`)
28  if (g.cacheMin !== null) parts.push(`◷ ${g.cacheMin}м`)
29  return parts.join('\u2003\u2003') // em-пробелы: обычные десктоп схлопывает в один
30}
31
32async function measure($: any) {
33  try {
34    const u = await $.session.usage()
35    const limits: any[] = u.rateLimits ?? []
36    const h5 = limits.find(l => l.kind === 'five_hour')?.percentUsed ?? null
37    const d7 = limits.find(l => l.kind === 'seven_day')?.percentUsed ?? null
38    const last = await read($, lastTurnAt)
39    const cacheMin = last > 0 ? Math.max(0, Math.round(CACHE_TTL_MIN - ((await $.clock.now()) - last) / 60000)) : null
40    const g: Gauge = { ctx: u.context?.percent ?? null, h5, d7, cacheMin }
41    await update($, gauge, () => g)
42    $.ui.status((await read($, isTerminal)) ? undefined : line(g) || undefined)
43  } catch (err: any) {
44    $.ui.status(`ошибка замера: ${String(err?.message ?? err).slice(0, 80)}`)
45  }
46}
47
48function color(p: number, warn: number, danger: number): string {
49  return p >= danger ? 'red' : p >= warn ? 'yellow' : 'green'
50}
51
52export const register: Register = on => {
53  on('session.start', async ($, e, next) => {
54    if (!timer) timer = $.clock.every(30000, () => void measure($))
55    void measure($)
56    return next(e)
57  })
58
59  on('turn.complete', async ($, e, next) => {
60    await update($, lastTurnAt, async () => await $.clock.now())
61    void measure($)
62    return next(e)
63  })
64
65  on('session.measure', async ($, e, next) => {
66    void measure($)
67    return next(e)
68  })
69
70  // Терминал: цветные шкалы в строке подсказки под вводом; пока человек печатает — подсказка движка
71  on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
72    if (e.surface !== 'terminal') return next(e)
73    if (!(await read($, isTerminal))) {
74      await update($, isTerminal, () => true)
75      $.ui.status(undefined)
76    }
77    if (e.props.isDraft) return next(e)
78    const g = await read($, gauge)
79    if (g.ctx === null && g.h5 === null && g.d7 === null) return next(e)
80    const { Box, Text } = $.ui.resolve(e)
81    // Узкий терминал: шкалы короче, на совсем узком — только числа; строка переносится, а не режется
82    const cols = e.viewport?.columns ?? 120
83    const n = cols >= 90 ? CELLS : cols >= 60 ? 4 : 0
84    const meter = (label: string, p: number | null, warn: number, danger: number) =>
85      p === null ? null : (
86        <Box marginRight={2}>
87          <Text dimColor>{label} </Text>
88          {n > 0 && <Text color={color(p, warn, danger)}>{cells(p, n)}</Text>}
89          <Text dimColor={n > 0} color={n > 0 ? undefined : color(p, warn, danger)}> {Math.round(p)}%</Text>
90        </Box>
91      )
92    return (
93      <Box flexWrap="wrap">
94        {meter('CTX', g.ctx, 45, 60)}
95        {meter('5H', g.h5, 70, 90)}
96        {meter('7D', g.d7, 70, 90)}
97        {g.cacheMin !== null && (
98          <Text color={g.cacheMin <= 5 ? 'red' : g.cacheMin <= 15 ? 'yellow' : undefined} dimColor={g.cacheMin > 15}>
99            ◷ {g.cacheMin}м
100          </Text>
101        )}
102      </Box>
103    )
104  })
105}
106
types/index.d.ts 8 lines
1export type Gauge = { ctx: number | null; h5: number | null; d7: number | null; cacheMin: number | null }
2
3declare module 'claude-code' {
4  interface PluginState {
5    ctx: { gauge: Gauge; lastTurnAt: number; isTerminal: boolean }
6  }
7}
8