Przycina ogromne wyniki Basha, PowerShella i MCP do początku, końca i linijek z błędami; pełny wynik zapisuje do pliku

Tu leży wszystko, co rozdaję na kanale. Za darmo, bez zapisywania się na cokolwiek.
Jak pobrać: kliknij zielony przycisk Code u góry, potem Download ZIP. Rozpakuj na pulpit. Nie musisz wiedzieć, co to GitHub — wystarczy ten jeden przycisk.
Skill to instrukcja, którą AI wykona. W czymkolwiek ściągniętym z internetu może być napisane wszystko. Sprawdzaj każdą paczkę, zanim ją wrzucisz. Moją też.
Paczka leży jeszcze w zwykłym folderze na pulpicie, więc otwórz nową rozmowę z AI w tym folderze i wklej:
Zanim tego użyję: przeczytaj te pliki jak zwykły tekst i powiedz mi po ludzku,
co ta instrukcja każe robić. Czy każe coś kasować, wysyłać gdzieś dane,
uruchamiać komendy albo grzebać poza swoim folderem?
Zasada kciuka: jeśli w instrukcji jest „wyślij", „skasuj" albo „uruchom" coś, czego nie rozumiesz — nie instalujesz. Nie musisz rozumieć wszystkiego. Wystarczy, że wiesz, kiedy powiedzieć „nie".
To nie jest kontrola pancerna, ale wyłapuje zdecydowaną większość śmieci. Różnica jak między wzięciem cukierka od obcego a przeczytaniem składu.
| Folder | Co to robi | Odcinek |
|---|---|---|
ghost/ | AI pisze maile i wiadomości twoim stylem, a nie jak robot | „Nauczyłem AI pisać jak ja" |
handoff/ | Kończysz rozmowę tak, żeby nowa wiedziała, na czym stanęliście — mniej zużytego limitu | „Znowu masz limit" |
krotko/ | Jedno słowo i z długiej odpowiedzi AI zostaje kilka zdań po ludzku | „AI gada za dużo" |
mody-limit/ | Trzech pomocników w Claude Code: pasek zużycia w kolorach, przycinanie gadatliwych wyników, stop dla AI kręcącego się w kółko | (odcinek w przygotowaniu) |
Każdy folder ma własny README.md z instrukcją krok po kroku.
Napisz w komentarzu pod odcinkiem — czytam wszystkie i z powtarzających się pytań robię następne filmy.
hooks/register.ts 151 lines1import { atom, update } from 'claude-code'
2import type { EngineInterface, Register, ToolCallResult } from 'claude-code'
3
4// Poniżej tego rozmiaru wynik idzie do Claude'a w całości.
5const MIN_CHARS = 8000
6// Co zostaje z dużego wyniku: początek, koniec i linijki z błędami ze środka.
7const HEAD_LINES = 40
8const HEAD_CHARS = 3000
9const TAIL_LINES = 60
10const TAIL_CHARS = 4000
11const MATCH_LINES = 30
12const MATCH_LINE_CHARS = 300
13const IMPORTANT = /error|warn|fail|fatal|exception|traceback|błąd|blad/i
14// Pełne wyniki krążą po 50 plikach, żeby folder nie puchł.
15const SLOTS = 50
16
17// Komendy, którymi Claude celowo czyta treść (plik, diff) — tych nie tniemy.
18const READS_CONTENT = /^\s*(cat|head|tail|sed|less|more|type|gc|get-content|jq|git\s+(diff|show))\b/i
19
20const savedChars = atom({ plugin: 'dieta-wyjsc', key: 'savedChars' } as const, 0)
21
22export const register: Register = on => {
23 on('session.start', async ($, e, next) => {
24 await $.command.register({
25 name: 'dieta',
26 description: 'Włącz albo wyłącz przycinanie dużych wyników narzędzi',
27 })
28
29 return next(e)
30 })
31
32 on('command.run', { command: 'dieta' }, async $ => {
33 const isOff = !((await $.store.get('isOff')) === true)
34 await $.store.set('isOff', isOff)
35 $.ui.status(isOff ? 'dieta: wyłączona' : undefined)
36
37 return { text: isOff ? 'Dieta wyłączona: wyniki narzędzi idą w całości.' : 'Dieta włączona.' }
38 })
39
40 on('tool.call', async ($, e, next) => {
41 const isShell = e.tool === 'Bash' || e.tool === 'PowerShell'
42 if (!isShell && !e.tool.startsWith('mcp__')) {
43 return next(e)
44 }
45 if (isShell && READS_CONTENT.test(String(e.command ?? ''))) {
46 return next(e)
47 }
48
49 const ran = await next(e)
50 if (ran.deny !== undefined || ran.isError === true || (await $.store.get('isOff')) === true) {
51 return ran
52 }
53
54 return isShell ? dietShell($, e.tool, ran) : dietMcp($, e.tool, ran)
55 })
56}
57
58type Shell = { stdout: string; stderr: string; isImage?: boolean; backgroundTaskId?: string; persistedOutputPath?: string }
59
60async function dietShell($: EngineInterface, tool: string, ran: ToolCallResult): Promise<ToolCallResult> {
61 const result = ran.result as Shell
62 // Obraz, zadanie w tle albo wynik, który Claude Code sam już odłożył do pliku.
63 if (typeof result?.stdout !== 'string' || result.isImage || result.backgroundTaskId || result.persistedOutputPath) {
64 return ran
65 }
66
67 const full = result.stderr ? `${result.stdout}\n--- stderr ---\n${result.stderr}` : result.stdout
68 const cut = await cutAndSave($, tool, full)
69
70 return cut === null ? ran : { result: { ...result, stdout: cut, stderr: '' } }
71}
72
73async function dietMcp($: EngineInterface, tool: string, ran: ToolCallResult): Promise<ToolCallResult> {
74 const cut = await cutAndSave($, tool, ran.text ?? '')
75
76 return cut === null ? ran : { result: cut }
77}
78
79// Przycięty tekst albo null, gdy nie ma czego ciąć lub nie udało się zapisać całości.
80async function cutAndSave($: EngineInterface, tool: string, full: string): Promise<string | null> {
81 if (full.length < MIN_CHARS) {
82 return null
83 }
84
85 const path = await saveFull($, tool, full)
86 if (path === null) {
87 return null
88 }
89
90 const cut = trim(full, path)
91 const saved = full.length - cut.length
92 if (saved <= 0) {
93 return null
94 }
95
96 const total = await update($, savedChars, n => n + saved)
97 $.ui.status(`dieta: −${Math.round(total / 4000)}k tokenów`)
98
99 return cut
100}
101
102async function saveFull($: EngineInterface, tool: string, full: string): Promise<string | null> {
103 try {
104 const home = (await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME'))
105 if (home === undefined) {
106 return null
107 }
108 const slot = Number((await $.store.get('slot')) ?? 0)
109 await $.store.set('slot', (slot + 1) % SLOTS)
110 const path = `${home.replace(/\\/g, '/')}/.claude/dieta/wynik-${String(slot).padStart(2, '0')}.log`
111 const stamp = new Date(await $.clock.now()).toISOString()
112 await $.fs.write(path, `# ${tool} · ${stamp}\n${full}`)
113 return path
114 } catch {
115 return null
116 }
117}
118
119export function trim(full: string, path: string): string {
120 const lines = full.split('\n')
121
122 // Dużo linii (log): tniemy po liniach. Mało, a długich (JSON w jednej linii): po znakach.
123 let head: string
124 let tail: string
125 let middle: string[]
126 if (lines.length > HEAD_LINES + TAIL_LINES) {
127 head = lines.slice(0, HEAD_LINES).join('\n').slice(0, HEAD_CHARS)
128 tail = lines.slice(-TAIL_LINES).join('\n').slice(-TAIL_CHARS)
129 middle = lines.slice(HEAD_LINES, -TAIL_LINES)
130 } else {
131 head = full.slice(0, HEAD_CHARS)
132 tail = full.slice(-TAIL_CHARS)
133 middle = full.slice(HEAD_CHARS, -TAIL_CHARS).split('\n')
134 }
135
136 const important = middle.filter(line => IMPORTANT.test(line))
137 const shown = important.slice(0, MATCH_LINES).map(line => line.slice(0, MATCH_LINE_CHARS))
138 const cutK = Math.round((full.length - head.length - tail.length) / 1000)
139
140 return [
141 head,
142 shown.length > 0
143 ? `… [dieta: wycięłam ~${cutK}k znaków ze środka; ${shown.length} z ${important.length} linijek z error/warn/fail poniżej]`
144 : `… [dieta: wycięłam ~${cutK}k znaków ze środka; nie było w nich error/warn/fail]`,
145 ...shown,
146 ...(shown.length > 0 ? ['… [dieta: dalej koniec wyniku]'] : []),
147 tail,
148 `[dieta: pełny wynik (${full.length} znaków) jest w ${path} — przeczytaj go Read albo grep, jeśli czegoś tu brakuje]`,
149 ].join('\n')
150}
151types/index.d.ts 12 lines1/** Liczba znaków, które dieta wycięła z wyników narzędzi. */
2export type SavedChars = number
3
4declare module 'claude-code' {
5 interface PluginState {
6 'dieta-wyjsc': {
7 /** Ile znaków dieta wycięła w tej sesji (do licznika na pasku statusu). */
8 savedChars: SavedChars
9 }
10 }
11}
12