Her projede: tasarım turları, takip, CI, bağlam koruma, hata hafızası, nerede kaldık

Claude Code kurulumum: her projede çalışan bir mod (genel), birkaç eklenti ve ayar. Yeni bir bilgisayarda tek komutla geri gelir.
git clone https://github.com/omeraydin00/claude-kurulum.git ~/Documents/GitHub/claude-kurulum
~/Documents/GitHub/claude-kurulum/kur.sh
Sonra yeni bir terminalde claude aç. macOS bildirimleri için ilk seferde Sistem Ayarları → Bildirimler → terminal-notifier → "Bildirimlere izin ver"i aç (stil: Kalıcı). Betik tekrar çalıştırılabilir: ~/.claude/settings.json'ın yedeğini alır ve yalnız aşağıdaki ayarları ekler, diğerlerine dokunmaz. Gerekenler: Claude Code 2.1.287+, python3, bun ya da node.
Kendi özel modların ayrı bir repodaysa onu bu reponun yanına claude-<ad> adıyla klonla (../claude-<ad>/mods/<mod>); kur.sh onları da yükler.
genel moduHer git reposunda, her dilde çalışır; parçaların çoğu yalnız gerektiğinde ortaya çıkar. /genel parçaları listeler; /genel kapat <parça> ve /genel ac <parça> ile açılıp kapanır, seçim kalıcıdır.
| Parça | Ne yapar |
|---|---|
| Nerede kaldık | Projeyi açınca bu klasördeki son oturumun iki cümlelik özeti ve "devam et" düğmesi |
| Proof of Done | Kod değişen turun sonunda projenin testleri (npm/pnpm/yarn/bun, pytest, go, cargo) arka planda koşar; kırılırsa hata Claude'a geri gönderilir (en çok 2 kez). Test komutu yoksa hiçbir şey yapmaz |
| Otomatik format | Düzenlenen dosyaya projenin kendi formatlayıcısı (Prettier, Ruff/Black, gofmt, rustfmt). Yalnız düzenlemeden önce zaten formatlı olan ve yeni dosyalarda çalışır; formatsız eski dosyayı baştan biçimlendirip PR’a ilgisiz fark sokmaz. Ayarı yoksa dokunmaz |
| Hata hafızası | Komut hata verince aynı hatayı eski oturum kayıtlarında arar, o zamanki açıklamayı Claude'a iletir. ripgrep (rg) varsa anında, yoksa arka planda arar |
| Bildirimler | 1 dakikadan uzun iş bitince ve Claude izin/cevap beklerken macOS bildirimi. Telefona da: /telefon (ntfy; içerik gönderilmez, yalnız proje adı). Sesli: /genel ac ses |
| Port çakışması | "port kullanımda" hatasında portu tutan programı gösterir, kapatma düğmesi verir |
.env denetimi | .env.example'da olup .env'de eksik değişkenleri söyler |
| CI izleyici | PR açınca ya da push edince testleri izler, bitince bildirir. main’e push testi kırılırsa prompt üstünde uyarır; [tamam] ile kapanır, yeni bir repo kırılınca tekrar çıkar |
| Bağlam koruma | Sıkıştırmada açık PR'lar, kararlar ve bekleyen sorular özete girer; sonra güncel git durumu verilir |
| Tur sonu kartı | Süre satırı yerine: ✓ 2 dk 14 sn · 3 dosya (+42 −10) · 2 komut |
| Takip | /takip: "cihazda denenmedi" ve "deploy sonrası yapılacak" listesi; Claude da ekleyebilir |
| Branch temizliği | /temizle: PR'ı merge olmuş yerel (ve fork'taki) branch'leri onayla siler, SHA'ları saklar |
| Tek tıklık cevap | Claude fiziksel bir adım isteyince (kabloyu tak vb.) prompt üstünde Yaptım / Olmadı düğmeleri |
| Telefon/tablet köprüsü | IP:port KOD 123456 yapıştırınca adb ile eşleştirip bağlar, Metro ve sunucu portlarını açar; /tablet |
| Ortam durumu | Açık geliştirme sunucuları ve telefondan erişilecek LAN adresi |
| Eklenti | Ne yapar |
|---|---|
claude-hud@claude-hud | Prompt altında tek satır: model, bağlam çubuğu, branch; limite yaklaşınca kullanım. Görünüm: /claude-hud:configure |
code-review@claude-plugins-official | PR'ı birkaç inceleyiciyle gözden geçirir |
cc-safety-net@cc-marketplace | rm -rf, force push gibi geri alınamayan komutları ve gizli dosya okumayı durdurur. Kontrol: npx -y cc-safety-net@latest doctor |
Kurulumsuz: maliyet raporu için npx ccusage@latest.
ayarlar/)settings-ekleri.json: mod yolu (CLAUDE_CODE_PLUGIN_DIRS), Türkçe yükleniyor yazıları (spinnerVerbs), claude-hud durum satırı (komut kurulumda bilgisayara göre üretilir)claude-hud-config.json: tek satırlık sade görünümEkranı dolduran ya da yerleşik özelliği tekrarlayanlar: tasarım turları (/rewind yapıyor), sade araç satırları (çalışan komutu gizliyordu), günlük özet bandı, bağlam göstergesi (claude-hud gösteriyor), security-guidance, claude-md-management, cache-tax ve vercel eklentileri.
claude plugin validate mods/genel
claude plugin test mods/genelhooks/register.tsx 147 lines1import type { Register } from 'claude-code'
2
3import { adimAraci, adimBant, adimSor, adimTemizle, ADIM_SOR } from './adim'
4import { acik, ayarKomut, ayarYukle, genelKomutu } from './ayar'
5import { bekliyorHaberi } from './bildirim'
6import { bittiHaberi } from './bitti'
7import { ciBaslat, ciSonrasi, kirmiziBant } from './ci'
8import { devirSikistir } from './devir'
9import { notEkle, notlariAl } from './durum'
10import { envBaslat, envDokunus } from './env'
11import { formatla, formatOncesi } from './format'
12import { hataHafizasi } from './hata'
13import { kaldikBaglami, kaldikBaslat, kaldikBant, kaldikKapat } from './kaldik'
14import { kartBaslat, kartCiz, kartKaydet, kartSay } from './kart'
15import { komutKlasoru, repoBilgisi } from './ortak'
16import { ortamBaslat } from './ortam'
17import { portBant, portBaslat, portCakismasi } from './port'
18import { tabletBaslat, tabletKaydet, tabletKomutu, tabletPrompt } from './tablet'
19import { TAKIP_EKLE, TAKIP_KAPAT, takipAraclari, takipEkle, takipKapat, takipKomut, takipKomutu, takipPrNotu, takipYukle } from './takip'
20import { telefonKomut, telefonKomutu } from './telefon'
21import { temizleKomut, temizleKomutu } from './temizlik'
22import { testIcinIsaretle, testKullaniciMesaji, testTuru } from './test'
23
24const DOSYA_ARACLARI = ['Edit', 'Write', 'MultiEdit', 'NotebookEdit']
25const ARAC = (ad: string) => new RegExp(`^mcp__genel__${ad}$`)
26const KOMUTLAR = [genelKomutu, tabletKomutu, takipKomutu, temizleKomutu, telefonKomutu]
27
28export const register: Register = on => {
29 on('session.start', async ($, e, next) => {
30 await ayarYukle($)
31 for (const komut of KOMUTLAR) await $.command.register(komut)
32 await takipYukle($)
33 await takipAraclari($).catch(() => {})
34 await adimAraci($).catch(() => {})
35 tabletBaslat($)
36 ciBaslat($)
37 ortamBaslat($)
38 portBaslat($)
39 // Ağ ve git işi bekletmesin: oturum hemen açılır, sonuçlar gelince bantta görünür.
40 kaldikBaslat($).catch(() => {})
41 envBaslat($).then(n => n.forEach(notEkle)).catch(() => {})
42 return next(e)
43 })
44
45 on('prompt.submit', async ($, e, next) => {
46 if (e.origin?.kind === 'composer') testKullaniciMesaji()
47 if (!e.isMeta && !e.command) {
48 kartBaslat()
49 await adimTemizle($)
50 const kaldik = await kaldikBaglami($)
51 if (kaldik) notEkle(kaldik)
52 await kaldikKapat($)
53 }
54 return tabletPrompt($, e, ev => {
55 const notlar = notlariAl()
56 return next(notlar.length ? { ...ev, context: [...(ev.context ?? []), ...notlar] } : ev)
57 })
58 })
59
60 tabletKaydet(on)
61
62 on('command.run', { command: 'genel' }, async ($, e) => ({ text: await ayarKomut($, e.args) }))
63 on('command.run', { command: 'takip' }, async ($, e) => ({ text: await takipKomut($, e.args) }))
64 on('command.run', { command: 'temizle' }, async $ => ({ text: await temizleKomut($) }))
65 on('command.run', { command: 'telefon' }, async ($, e) => ({ text: await telefonKomut($, e.args) }))
66
67 on('turn.complete', async ($, e, next) => {
68 if (!e.agentId) {
69 await kartKaydet($, e.durationMs).catch(() => {})
70 // Testler uzun sürebilir: tur bekletilmez, sonuç gelince bildirilir.
71 if (!e.isAborted) testTuru($).catch(() => {})
72 }
73 const r = await next(e)
74 await bittiHaberi($, e, r.text).catch(() => {})
75 return r
76 })
77
78 on('session.compact', ($, e, next) => devirSikistir($, e, next))
79
80 // İzin ya da cevap beklerken Claude Code'un kendi bildirim olayı.
81 on('classic.Notification', async ($, e, next) => {
82 await bekliyorHaberi($, e).catch(() => {})
83 return next(e)
84 })
85
86 // Claude Code'un süre satırı yerine tur kartı.
87 on('ui.render', { component: 'TurnDuration' }, async ($, e, next) => (await kartCiz($, e).catch(() => null)) ?? next(e))
88
89 // Prompt üstü: düğmeler, port uyarısı, nerede kaldık, main kırmızı; altında diğer eklentilerin bandı.
90 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
91 const benim = [await adimBant($, e), await portBant($, e), await kaldikBant($, e), await kirmiziBant($, e)].filter(p => p !== null)
92 if (benim.length === 0) return next(e)
93 const { Box } = $.ui.resolve(e)
94 return (
95 <Box flexDirection="column">
96 {benim}
97 {await next(e)}
98 </Box>
99 )
100 })
101
102 // Modun Claude'a açtığı araçlar.
103 on('tool.call', { tool: ARAC(TAKIP_EKLE) }, async ($, e) => ({ result: await takipEkle($, e as Record<string, unknown>) }))
104 on('tool.call', { tool: ARAC(TAKIP_KAPAT) }, async ($, e) => ({ result: await takipKapat($, Number((e as Record<string, unknown>).id)) }))
105 on('tool.call', { tool: ARAC(ADIM_SOR) }, async ($, e) => ({ result: await adimSor($, e as Record<string, unknown>) }))
106
107 on('tool.call', async ($, e, next) => {
108 if (e.tool.startsWith('mcp__genel__')) return next(e)
109 const yol = DOSYA_ARACLARI.includes(e.tool) && 'file_path' in e && typeof e.file_path === 'string' ? e.file_path : null
110 const notlar: string[] = []
111 if (yol && !e.agentId) {
112 const n = await envDokunus($, yol).catch(() => null)
113 if (n) notlar.push(n)
114 if (['Edit', 'MultiEdit', 'Write'].includes(e.tool)) await formatOncesi($, yol).catch(() => {})
115 }
116
117 const ran = await next(e)
118 if (ran.deny !== undefined) return ran
119 // Komut çalıştı: buradan sonra bir hata komutu ikinci kez çalıştırmasın diye sonuç her durumda döner.
120 try {
121 if (!e.agentId) kartSay(e, Boolean(ran.isError))
122 if (yol && !e.agentId && !ran.isError && ['Edit', 'MultiEdit', 'Write'].includes(e.tool)) {
123 const f = await formatla($, yol).catch(() => null)
124 if (f) notlar.push(f)
125 await testIcinIsaretle($, yol).catch(() => {})
126 }
127 if (e.tool === 'Bash') {
128 const sonuc = (ran.result ?? {}) as { stdout?: unknown; stderr?: unknown }
129 const cikti = ran.text ?? `${sonuc.stdout ?? ''}\n${sonuc.stderr ?? ''}`
130 ciSonrasi($, e.command, cikti).catch(() => {})
131 if (ran.isError && !e.agentId) {
132 for (const n of await Promise.all([portCakismasi($, cikti), hataHafizasi($, cikti)].map(p => p.catch(() => null)))) if (n) notlar.push(n)
133 }
134 if (/\bgh\s+pr\s+create\b/.test(e.command) && (await acik($, 'takip'))) {
135 const r = await repoBilgisi($, komutKlasoru(e.command, await $.session.cwd()))
136 const tk = await takipPrNotu($, r?.ad)
137 if (tk) notlar.push(tk)
138 }
139 }
140 notlar.push(...notlariAl())
141 return notlar.length ? { ...ran, context: [...(ran.context ?? []), ...notlar] } : ran
142 } catch {
143 return ran
144 }
145 }).catch(($, e, next) => next(e))
146}
147hooks/adim.tsx 66 lines1import { atom, read, update } from 'claude-code'
2import type { RenderInput } from 'claude-code'
3
4import { acik } from './ayar'
5import type { $T } from './ortak'
6
7export const adim = atom({ plugin: 'genel', key: 'adim' } as const, null)
8
9export const ADIM_SOR = 'adim_sor'
10const VARSAYILAN_SECENEK = ['Yaptım', 'Olmadı']
11
12export async function adimAraci($: $T) {
13 await $.tool.register({
14 name: ADIM_SOR,
15 description:
16 'Kullanıcıdan fiziksel bir adım istediğinde kullan (kabloyu tak, butona bas, tableti yeniden başlat, fincanı koy). ' +
17 'Prompt’un üstünde tek tıklık cevap düğmeleri gösterir; kullanıcı basınca cevap sana yeni mesaj olarak gelir. ' +
18 'Çağırdıktan sonra turu bitir ve bekle. secenekler verilmezse "Yaptım" ve "Olmadı" çıkar.',
19 inputSchema: {
20 type: 'object',
21 properties: {
22 soru: { type: 'string', description: 'Kısa Türkçe talimat, ör. "Kartın USB kablosunu tak"' },
23 secenekler: { type: 'array', items: { type: 'string' }, maxItems: 4, description: 'ör. ["Taktım", "Işık yanmıyor", "Olmadı"]' },
24 },
25 required: ['soru'],
26 },
27 isDeferred: false,
28 })
29}
30
31export async function adimSor($: $T, girdi: { soru?: unknown; secenekler?: unknown }): Promise<string> {
32 if (!(await acik($, 'adim'))) return 'Düğmeler kapalı (/genel ac adim); kullanıcıya düz metinle sor.'
33 const secenekler = Array.isArray(girdi.secenekler) ? girdi.secenekler.map(String).filter(Boolean).slice(0, 4) : []
34 await update($, adim, () => ({ soru: String(girdi.soru ?? ''), secenekler: secenekler.length ? secenekler : VARSAYILAN_SECENEK }))
35 return 'Düğmeler gösterildi. Turu burada bitir; kullanıcının cevabı yeni mesaj olarak gelecek.'
36}
37
38// Kullanıcı kendi yazarak cevap verirse düğmeler kalkar.
39export async function adimTemizle($: $T) {
40 if (await read($, adim)) await update($, adim, () => null)
41}
42
43async function cevapla($: $T, metin: string) {
44 await update($, adim, () => null)
45 await $.prompt.submit({ text: metin, asUser: true })
46}
47
48export async function adimBant($: $T, e: RenderInput<'AbovePrompt'>) {
49 const a = await read($, adim)
50 if (!a) return null
51 const { Box, Button, Text } = $.ui.resolve(e)
52 return (
53 <Box flexDirection="column">
54 <Text color="suggestion" wrap="truncate-end">🔧 {a.soru}</Text>
55 <Box>
56 {a.secenekler.map((s, i) => (
57 <Box key={`adim-${i}`}>
58 <Button key={`adim-b-${i}`} label={s} onPress={() => cevapla($, s)} />
59 <Text> </Text>
60 </Box>
61 ))}
62 </Box>
63 </Box>
64 )
65}
66hooks/ayar.ts 76 lines1import { atom, read, update } from 'claude-code'
2
3import type { Ayarlar } from '../types'
4import type { $T } from './ortak'
5
6export type Parca = keyof Ayarlar
7
8export const PARCALAR: Record<Parca, string> = {
9 kaldik: 'Nerede kaldık: projeyi açınca son oturumun özeti ve yarım kalan iş',
10 hata: 'Hata hafızası: aynı hata eski oturumlarda çözüldüyse hatırlatır',
11 bitti: 'Bitince haber ver: uzun iş bitince bildirim',
12 ses: 'Sesli haber: bitince "bitti", beklerken "seni bekliyor" der',
13 bekliyor: 'Seni bekliyor: izin ya da cevap beklerken masaüstü bildirimi',
14 kart: 'Tur sonu kartı: süre, dosya, satır ve komut sayısı',
15 format: 'Otomatik format: düzenlenen dosyaya projenin formatlayıcısı',
16 test: 'Proof of Done: kod değişen turun sonunda testler; kırılırsa Claude düzeltir',
17 telefon: 'Telefona bildirim (ntfy): /telefon ile kurulur',
18 takip: 'Takip: denenmedi / deploy sonrası listesi (/takip)',
19 adim: 'Tek tıklık cevap düğmeleri: Yaptım / Olmadı',
20 devir: 'Bağlam koruma: sıkıştırmada PR’lar, kararlar ve sorular korunur',
21 ci: 'CI izleyici ve main kırmızı uyarısı',
22 tablet: 'Telefon/tablet köprüsü: "IP:port kod" yapıştırınca bağlanır',
23 ortam: 'Ortam: açık dev sunucuları ve telefondan erişilecek adres',
24 port: 'Port çakışması: portu tutan programı gösterir, kapatma düğmesi',
25 env: '.env denetimi: .env.example’da olup .env’de eksik değişkenler',
26}
27
28const VARSAYILAN: Ayarlar = {
29 kaldik: true, hata: true, bitti: true, ses: false, bekliyor: true, kart: true, format: true, test: true, telefon: false, takip: true, adim: true,
30 devir: true, ci: true, tablet: true, ortam: true, port: true, env: true,
31}
32
33export const ayarlar = atom({ plugin: 'genel', key: 'ayarlar' } as const, VARSAYILAN)
34
35export async function ayarYukle($: $T) {
36 const kayitli = (await $.store.get('ayarlar')) as Partial<Ayarlar> | undefined
37 await update($, ayarlar, () => ({ ...VARSAYILAN, ...kayitli }))
38}
39
40export async function acik($: $T, parca: Parca): Promise<boolean> {
41 return (await read($, ayarlar))[parca]
42}
43
44// Komutların açıp kapattığı tek parça (ör. /telefon).
45export async function parcaAyarla($: $T, parca: Parca, deger: boolean) {
46 const yeni = { ...(await read($, ayarlar)), [parca]: deger }
47 await update($, ayarlar, () => yeni)
48 await $.store.set('ayarlar', yeni)
49}
50
51export const genelKomutu = {
52 name: 'genel',
53 description: 'genel modunun parçalarını listeler; "/genel kapat test", "/genel ac ses", "/genel kapat hepsi"',
54}
55
56function liste(a: Ayarlar): string {
57 return (Object.keys(PARCALAR) as Parca[]).map(p => `${a[p] ? '✓' : '✗'} ${p.padEnd(8)} ${PARCALAR[p]}`).join('\n')
58}
59
60export async function ayarKomut($: $T, args: string): Promise<string> {
61 const [eylem = '', ...adlar] = args.trim().toLocaleLowerCase('tr').split(/\s+/).filter(Boolean)
62 const simdiki = await read($, ayarlar)
63 if (!eylem) return `${liste(simdiki)}\n\nDeğiştirmek için: /genel kapat <parça> · /genel ac <parça> (hepsi de olur)`
64
65 const deger = ['ac', 'aç', 'on'].includes(eylem) ? true : ['kapat', 'off'].includes(eylem) ? false : null
66 const hedef = adlar.includes('hepsi') ? (Object.keys(PARCALAR) as Parca[]) : adlar.filter((a): a is Parca => a in PARCALAR)
67 const bilinmeyen = adlar.filter(a => a !== 'hepsi' && !(a in PARCALAR))
68 if (deger === null || hedef.length === 0 || bilinmeyen.length > 0) {
69 return `Anlamadım${bilinmeyen.length ? `: ${bilinmeyen.join(', ')}` : ''}. Parçalar: ${Object.keys(PARCALAR).join(', ')}, hepsi.\nÖrnek: /genel kapat test`
70 }
71 const yeni = { ...simdiki, ...Object.fromEntries(hedef.map(p => [p, deger])) } as Ayarlar
72 await update($, ayarlar, () => yeni)
73 await $.store.set('ayarlar', yeni)
74 return `${hedef.join(', ')} ${deger ? 'açıldı' : 'kapatıldı'} (sonraki oturumlarda da geçerli).\n\n${liste(yeni)}`
75}
76hooks/bildirim.ts 38 lines1import { acik } from './ayar'
2import { telefonGonder } from './telefon'
3import { adi, calistir } from './ortak'
4import type { $T } from './ortak'
5
6const TERMINAL_NOTIFIER = ['/opt/homebrew/bin/terminal-notifier', '/usr/local/bin/terminal-notifier']
7
8// macOS bildirimi: $.ui.notify VS Code terminalinde yalnız zil çalıyor ve classic.Notification'ı yeniden tetikliyor.
9// terminal-notifier varsa o (Bildirimler'de kendi adıyla izin alır); yoksa osascript, o da olmazsa Claude Code'un kendi bildirimi.
10export async function macBildirim($: $T, baslik: string, metin: string) {
11 let r = { ok: false }
12 for (const yol of TERMINAL_NOTIFIER) {
13 if (!(await $.fs.exists(yol))) continue
14 r = await calistir($, [yol, '-title', baslik, '-message', metin.slice(0, 200) || ' ', '-sound', 'default', '-group', 'claude-genel'])
15 break
16 }
17 if (!r.ok) {
18 r = await calistir($, [
19 'osascript', '-e', 'on run argv', '-e', 'display notification (item 1 of argv) with title (item 2 of argv)', '-e', 'end run',
20 metin.slice(0, 200), baslik,
21 ])
22 }
23 if (!r.ok) await $.ui.notify(metin.slice(0, 140), { title: baslik }).catch(() => {})
24 await telefonGonder($, baslik, adi(await $.session.cwd()))
25 if (await acik($, 'ses')) await $.audio.speak(baslik.startsWith('Claude bitti') ? 'Bitti' : 'Seni bekliyor').catch(() => {})
26}
27
28let sonBildirim = 0
29
30// Claude izin isterken ya da cevap beklerken (classic Notification olayı).
31export async function bekliyorHaberi($: $T, e: { message?: string }) {
32 if (!(await acik($, 'bekliyor'))) return
33 const simdi = await $.clock.now()
34 if (simdi - sonBildirim < 30_000) return
35 sonBildirim = simdi
36 await macBildirim($, 'Claude seni bekliyor', e.message || 'Cevabın ya da onayın gerekiyor')
37}
38hooks/bitti.ts 16 lines1import type { TurnCompleteInput } from 'claude-code'
2
3import { acik } from './ayar'
4import { macBildirim } from './bildirim'
5import type { $T } from './ortak'
6
7const UZUN_MS = 60_000
8
9// Uzun süren tur bitince bildirim: ekran başında beklemek gerekmesin.
10export async function bittiHaberi($: $T, e: TurnCompleteInput, cevap: string) {
11 if (e.agentId || e.isAborted || e.durationMs < UZUN_MS || !(await acik($, 'bitti'))) return
12 const ilk = cevap.split('\n').map(s => s.replace(/[*#`>|]/g, '').trim()).find(Boolean) ?? 'Hazır'
13 const dk = Math.round(e.durationMs / 60_000)
14 await macBildirim($, `Claude bitti (${dk} dk)`, ilk)
15}
16hooks/ci.tsx 146 lines1import { atom, read, update } from 'claude-code'
2import type { RenderInput } from 'claude-code'
3
4import type { CiIzleme } from '../types'
5import { acik } from './ayar'
6import { durumYaz, notEkle } from './durum'
7import { calistir, dosyaOku, gitKlasoru, kapsamRepolar, repoBilgisi } from './ortak'
8import type { $T } from './ortak'
9
10export const ci = atom({ plugin: 'genel', key: 'ci' } as const, [])
11const mainKirmizi = atom({ plugin: 'genel', key: 'mainKirmizi' } as const, [])
12// [tamam]'a basılınca o anki liste; aynı liste sürdükçe bant gizli kalır, yeni repo kırmızıya dönerse yeniden çıkar.
13const kirmiziGorulen = atom({ plugin: 'genel', key: 'kirmiziGorulen' } as const, '')
14
15const KONTROL_YOK_SN = 5 * 60
16const EN_UZUN_SN = 60 * 60
17
18type Kontrol = { name: string; bucket: string }
19
20async function izle($: $T, slug: string, no: number, url: string) {
21 const basladi = await $.clock.now()
22 const repo = slug.split('/')[1] ?? slug
23 await update($, ci, liste => [...liste.filter(i => !(i.slug === slug && i.no === no)), { slug, repo, no, url, basladi, ozet: null }])
24 durumYaz($, 'ci', `CI ${repo} #${no} ⏳`)
25}
26
27// gh pr create'in çıktısındaki PR adresi ya da push edilen branch'in açık PR'ı izlemeye alınır.
28export async function ciSonrasi($: $T, komut: string, stdout: string) {
29 if (!(await acik($, 'ci'))) return
30 const adres = stdout.match(/https:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/pull\/(\d+)/)
31 if (/\bgh\s+pr\s+create\b/.test(komut) && adres) return izle($, adres[1]!, Number(adres[2]), adres[0])
32
33 const push = komut.match(/\bgit\b(?:\s+-C\s+\S+)?\s+push\b([^;&|]*)/)
34 if (!push || /--delete|--tags|\s-d\b/.test(push[1]!)) return
35 const r = await repoBilgisi($, gitKlasoru(komut, await $.session.cwd()))
36 if (!r?.slug) return
37 const konum = push[1]!.trim().split(/\s+/).filter(a => a && !a.startsWith('-'))
38 const branch = konum[1]?.split(':').pop() ?? (await calistir($, ['git', 'branch', '--show-current'], r.dir)).out
39 if (!branch || branch === 'main') return
40 const s = await calistir($, ['gh', 'pr', 'list', '--repo', r.slug, '--head', branch, '--state', 'open', '--json', 'number,url'])
41 const pr = s.ok ? (JSON.parse(s.out || '[]') as { number: number; url: string }[])[0] : undefined
42 if (pr) await izle($, r.slug, pr.number, pr.url)
43}
44
45async function sonuc($: $T, i: CiIzleme, metin: string, kirmizi: boolean) {
46 $.ui.toast(metin, { timeoutMs: 10_000 })
47 await $.ui.notify(metin, { title: kirmizi ? 'CI kırmızı' : 'CI yeşil' }).catch(() => {})
48 notEkle(`[genel/ci] ${metin} (${i.url})${kirmizi ? ' Kullanıcıya söyle; hata için: gh run view --repo ' + i.slug + ' --log-failed' : ''}`)
49}
50
51async function tur($: $T) {
52 const liste = await read($, ci)
53 if (liste.length === 0) return
54 const simdi = await $.clock.now()
55 const kalan: CiIzleme[] = []
56 for (const i of liste) {
57 const gecen = (simdi - i.basladi) / 1000
58 const s = await calistir($, ['gh', 'pr', 'checks', String(i.no), '--repo', i.slug, '--json', 'name,bucket'])
59 let kontroller: Kontrol[] = []
60 try {
61 kontroller = JSON.parse(s.out || '[]') as Kontrol[]
62 } catch {
63 kontroller = []
64 }
65 if (kontroller.length === 0) {
66 // Bu repoda CI yok ya da henüz başlamadı.
67 if (gecen < KONTROL_YOK_SN) kalan.push(i)
68 continue
69 }
70 const bekleyen = kontroller.filter(k => k.bucket === 'pending')
71 if (bekleyen.length > 0) {
72 if (gecen < EN_UZUN_SN) kalan.push({ ...i, ozet: `${kontroller.length - bekleyen.length}/${kontroller.length}` })
73 else $.ui.toast(`CI ${i.repo} #${i.no} bir saattir bitmedi; izleme bırakıldı`)
74 continue
75 }
76 const kirmizi = kontroller.filter(k => k.bucket === 'fail' || k.bucket === 'cancel')
77 if (kirmizi.length > 0) await sonuc($, i, `⛔ ${i.repo} PR #${i.no}: CI kırmızı (${kirmizi.map(k => k.name).join(', ')})`, true)
78 else await sonuc($, i, `✅ ${i.repo} PR #${i.no}: CI yeşil (${kontroller.length} kontrol)`, false)
79 }
80 await update($, ci, () => kalan)
81 durumYaz($, 'ci', kalan.length ? 'CI ' + kalan.map(i => `${i.repo} #${i.no} ⏳${i.ozet ? ' ' + i.ozet : ''}`).join(', ') : undefined)
82}
83
84// main'in son tamamlanan CI koşusu: kırmızıya dönen repo bildirilir, bantta durur.
85let mainSuruyor = false
86
87async function mainTur($: $T) {
88 if (mainSuruyor) return
89 mainSuruyor = true
90 try {
91 await mainKontrol($)
92 } finally {
93 mainSuruyor = false
94 }
95}
96
97async function mainKontrol($: $T) {
98 if (!(await acik($, 'ci'))) return update($, mainKirmizi, () => [])
99 const kirmizi: string[] = []
100 for (const r of (await kapsamRepolar($)).filter(x => x.slug)) {
101 // Yalnız main'e push'la tetiklenen koşular: Dependabot ve zamanlanmış işler main'in sağlığını göstermez.
102 const s = await calistir($, ['gh', 'run', 'list', '--repo', r.slug, '--branch', 'main', '--event', 'push', '--status', 'completed', '--limit', '1', '--json', 'conclusion'])
103 try {
104 const son = (JSON.parse(s.out || '[]') as { conclusion: string }[])[0]
105 if (son && ['failure', 'timed_out', 'startup_failure'].includes(son.conclusion)) kirmizi.push(r.ad)
106 } catch {}
107 }
108 // Bildirilen liste diskte: mod yeniden yüklenince aynı kırmızı tekrar "yeni" sayılmasın.
109 const dosya = `${(await $.env.get('HOME')) ?? '/tmp'}/.claude/genel/ci-bildirilen.json`
110 let once: string[] = []
111 try {
112 once = JSON.parse((await dosyaOku($, dosya)) ?? '[]') as string[]
113 } catch {}
114 const yeni = kirmizi.filter(k => !once.includes(k))
115 if (kirmizi.join(',') !== once.join(',')) await $.fs.write(dosya, JSON.stringify(kirmizi)).catch(() => {})
116 if (yeni.length) {
117 $.ui.toast(`⛔ main kırmızı: ${yeni.join(', ')}`, { timeoutMs: 10_000 })
118 notEkle(`[genel/ci] ${yeni.join(', ')} main CI'ı kırmızı; bu repolarda PR merge etmeden önce kullanıcıya söyle.`)
119 }
120 await update($, mainKirmizi, () => kirmizi)
121}
122
123export function ciBaslat($: $T) {
124 $.clock.every(60_000, () => tur($).catch(() => {}))
125 mainTur($).catch(() => {})
126 $.clock.every(10 * 60_000, () => mainTur($).catch(() => {}))
127}
128
129export async function ciOzeti($: $T): Promise<string | null> {
130 const liste = await read($, ci)
131 return liste.length ? liste.map(i => `${i.repo} PR #${i.no} CI sürüyor`).join(', ') : null
132}
133
134export async function kirmiziBant($: $T, e: RenderInput<'AbovePrompt'>) {
135 const kirmizi = await read($, mainKirmizi)
136 const gorulen = (await read($, kirmiziGorulen)).split(',')
137 if (kirmizi.length === 0 || kirmizi.every(k => gorulen.includes(k))) return null
138 const { Box, Button, Text } = $.ui.resolve(e)
139 return (
140 <Box>
141 <Text color="error" wrap="truncate-end">⛔ main kırmızı: {kirmizi.join(', ')} </Text>
142 <Button key="kirmizi-tamam" plain label="[tamam]" onPress={() => update($, kirmiziGorulen, () => kirmizi.join(','))} />
143 </Box>
144 )
145}
146hooks/devir.ts 54 lines1import type { SessionCompactInput, SessionCompactResult } from 'claude-code'
2
3import { acik } from './ayar'
4import { ciOzeti } from './ci'
5import { notEkle } from './durum'
6import { calistir, kapsamRepolar } from './ortak'
7import type { $T } from './ortak'
8import { tabletAl } from './tablet'
9import { takipAl, takipSatiri } from './takip'
10
11// Sabit metin: precompute'un hazırladığı özet asıl sıkıştırmada da geçerli kalsın.
12const TALIMAT = `[genel/devir] Özette ayrı bir "Devir" bölümü olsun ve şunları eksiksiz, numaralarıyla koru:
13- Açık ya da açılması bekleyen PR'lar: repo, numara, branch, durum (taslak/review/CI), birbirine bağlantıları.
14- Kullanıcının ve yöneticinin istekleri, review yorumları ve henüz yapılmamış olanlar.
15- Kullanıcının verdiği kararlar ve onayladığı sürüm numaraları.
16- Cihazda / tarayıcıda henüz denenmemiş değişiklikler ve deploy sonrası yapılacaklar.
17- Kullanıcının sorduğu ama cevabı bekleyen sorular.`
18
19export function talimatEkle(mevcut: string | undefined): string {
20 return [mevcut, TALIMAT].filter(Boolean).join('\n\n')
21}
22
23// Sıkıştırmadan hemen sonra Claude'a verilen canlı durum: modun kendi ölçtükleri.
24export async function devirDurumu($: $T): Promise<string> {
25 const satirlar: string[] = []
26 for (const r of await kapsamRepolar($)) {
27 const branch = (await calistir($, ['git', 'branch', '--show-current'], r.dir)).out
28 const degisik = (await calistir($, ['git', 'status', '--porcelain'], r.dir)).out.split('\n').filter(Boolean).length
29 if (branch && (branch !== 'main' && branch !== 'master' || degisik)) satirlar.push(`${r.ad}: ${branch}${degisik ? ` ✎${degisik} dosya` : ''}`)
30 }
31 const ciDurum = await ciOzeti($)
32 if (ciDurum) satirlar.push(ciDurum)
33 const t = await tabletAl($)
34 if (t) satirlar.push(`tablet: ${t.model} ${t.serial} ${t.bagli ? 'bağlı' : 'kopuk'}, Metro ${t.metro}`)
35 const tk = await takipAl($)
36 if (tk.length) satirlar.push(`takip (/takip): ${tk.map(takipSatiri).join('; ')}`)
37 return satirlar.length ? satirlar.join('\n') : 'açık iş görünmüyor'
38}
39
40export async function devirSikistir(
41 $: $T,
42 e: SessionCompactInput,
43 next: (e: SessionCompactInput) => Promise<SessionCompactResult>,
44): Promise<SessionCompactResult> {
45 if (e.agentId || !(await acik($, 'devir'))) return next(e)
46 // Konuşması verilmeyen çağrıda (ör. test) talimat eklenemez; sıkıştırma olduğu gibi sürer.
47 const r = await next(Array.isArray(e.messages) ? { ...e, instructions: talimatEkle(e.instructions) } : e)
48 if (r.skip === undefined && e.trigger !== 'precompute') {
49 const durum = await devirDurumu($)
50 notEkle(`[genel/devir] Konuşma az önce sıkıştırıldı. Repoların şu anki durumu (mod git/gh'den ölçtü, özetten daha güncel):\n${durum}`)
51 }
52 return r
53}
54hooks/durum.ts 25 lines1import type { $T } from './ortak'
2
3// Durum satırı tek (eklenti başına bir tane): parçalar yan yana.
4type Anahtar = 'test' | 'tablet' | 'ortam' | 'ci'
5const SIRA: Anahtar[] = ['test', 'tablet', 'ortam', 'ci']
6const parcalar: Partial<Record<Anahtar, string>> = {}
7
8export function durumYaz($: $T, anahtar: Anahtar, metin: string | undefined) {
9 if (parcalar[anahtar] === metin) return
10 parcalar[anahtar] = metin
11 const satir = SIRA.map(a => parcalar[a]).filter(Boolean).join(' · ')
12 $.ui.status(satir || undefined)
13}
14
15// Claude'a bir sonraki fırsatta (prompt ya da araç sonucu) iletilecek notlar.
16const bekleyen: string[] = []
17
18export function notEkle(metin: string) {
19 if (!bekleyen.includes(metin)) bekleyen.push(metin)
20}
21
22export function notlariAl(): string[] {
23 return bekleyen.splice(0)
24}
25hooks/env.ts 57 lines1import { acik } from './ayar'
2import { dosyaOku, kapsamRepolar, repoBilgisi } from './ortak'
3import type { $T } from './ortak'
4
5const ORNEKLER = ['.env.example', '.env.sample', '.env.template', '.env.dist']
6const bakilan = new Set<string>()
7
8export function anahtarlar(metin: string | null): Set<string> {
9 const s = new Set<string>()
10 for (const satir of (metin ?? '').split('\n')) {
11 const m = satir.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/)
12 if (m) s.add(m[1]!)
13 }
14 return s
15}
16
17// .env.example'da olup .env / .env.local'da olmayan değişkenler.
18export async function envEksik($: $T, dir: string): Promise<string | null> {
19 for (const ornek of ORNEKLER) {
20 const metin = await dosyaOku($, `${dir}/${ornek}`)
21 if (metin === null) continue
22 const env = await dosyaOku($, `${dir}/.env`)
23 const yerel = await dosyaOku($, `${dir}/.env.local`)
24 if (env === null && yerel === null) return `.env yok (${ornek} var)`
25 const var_ = new Set([...anahtarlar(env), ...anahtarlar(yerel)])
26 const eksik = [...anahtarlar(metin)].filter(k => !var_.has(k))
27 return eksik.length ? `.env'de eksik: ${eksik.join(', ')}` : null
28 }
29 return null
30}
31
32async function bak($: $T, dir: string, ad: string): Promise<string | null> {
33 if (bakilan.has(dir)) return null
34 bakilan.add(dir)
35 const sonuc = await envEksik($, dir)
36 if (!sonuc) return null
37 $.ui.toast(`⚙ ${ad}: ${sonuc}`, { timeoutMs: 8_000 })
38 return `[genel/env] ${ad}: ${sonuc}. Bağlantı / yapılandırma hatası görürsen önce buna bak; değerleri kullanıcıya sor.`
39}
40
41export async function envBaslat($: $T): Promise<string[]> {
42 if (!(await acik($, 'env'))) return []
43 const notlar: string[] = []
44 for (const r of (await kapsamRepolar($)).slice(0, 15)) {
45 const n = await bak($, r.dir, r.ad)
46 if (n) notlar.push(n)
47 }
48 return notlar
49}
50
51// Oturumda ilk kez dokunulan repo için.
52export async function envDokunus($: $T, yol: string): Promise<string | null> {
53 if (!(await acik($, 'env'))) return null
54 const r = await repoBilgisi($, yol.slice(0, yol.lastIndexOf('/')) || '/')
55 return r ? bak($, r.dir, r.ad) : null
56}
57hooks/format.ts 57 lines1import { acik } from './ayar'
2import { adi, calistir, dosyaOku, repoBilgisi } from './ortak'
3import type { $T } from './ortak'
4
5// Yalnız projenin kendi kullandığı formatlayıcı: yapılandırması ya da bağımlılığı yoksa dokunulmaz.
6const PRETTIER_UZANTI = /\.(tsx?|jsx?|mjs|cjs|css|scss|json|md|html|ya?ml|vue|svelte)$/
7const PRETTIER_AYAR = ['.prettierrc', '.prettierrc.json', '.prettierrc.js', '.prettierrc.cjs', '.prettierrc.mjs', '.prettierrc.yaml', '.prettierrc.yml', 'prettier.config.js', 'prettier.config.mjs', 'prettier.config.cjs']
8
9type Bicimci = { yaz: string[]; denetle: string[] }
10
11export async function formatlayici($: $T, kok: string, yol: string): Promise<Bicimci | null> {
12 if (PRETTIER_UZANTI.test(yol)) {
13 const pkg = (await dosyaOku($, `${kok}/package.json`)) ?? ''
14 const ayarli = /"prettier"\s*:/.test(pkg) || (await Promise.all(PRETTIER_AYAR.map(a => $.fs.exists(`${kok}/${a}`)))).some(Boolean)
15 const p = `${kok}/node_modules/.bin/prettier`
16 if (ayarli && (await $.fs.exists(p))) return { yaz: [p, '--write', '--log-level', 'warn', yol], denetle: [p, '--check', '--log-level', 'silent', yol] }
17 return null
18 }
19 if (yol.endsWith('.py')) {
20 const py = (await dosyaOku($, `${kok}/pyproject.toml`)) ?? ''
21 if (/\[tool\.ruff/.test(py) || (await $.fs.exists(`${kok}/ruff.toml`))) return { yaz: ['ruff', 'format', yol], denetle: ['ruff', 'format', '--check', yol] }
22 if (/\[tool\.black\]/.test(py)) return { yaz: ['black', '-q', yol], denetle: ['black', '-q', '--check', yol] }
23 return null
24 }
25 if (yol.endsWith('.go')) return { yaz: ['gofmt', '-w', yol], denetle: ['sh', '-c', 'test -z "$(gofmt -l "$1")"', 'sh', yol] }
26 if (yol.endsWith('.rs') && (await $.fs.exists(`${kok}/Cargo.toml`))) return { yaz: ['rustfmt', '--edition', '2021', yol], denetle: ['rustfmt', '--edition', '2021', '--check', yol] }
27 return null
28}
29
30// Düzenlemeden önce: dosya zaten formatlıysa (ya da yeni dosyaysa) düzenlemeden sonra formatlanır.
31// Formatsız dosyayı baştan sona biçimlendirmek PR'da ilgisiz yüzlerce satır fark çıkarır.
32const formatlanacak = new Set<string>()
33
34export async function formatOncesi($: $T, yol: string) {
35 formatlanacak.delete(yol)
36 if (!(await acik($, 'format'))) return
37 const r = await repoBilgisi($, yol.slice(0, yol.lastIndexOf('/')) || '/')
38 const b = r && (await formatlayici($, r.dir, yol))
39 if (!r || !b) return
40 if (!(await $.fs.exists(yol)) || (await calistir($, b.denetle, r.dir, 30_000)).ok) formatlanacak.add(yol)
41}
42
43// Düzenlemeden sonra: dosya değiştiyse Claude bir sonraki düzenlemeden önce yeniden okumalı.
44export async function formatla($: $T, yol: string): Promise<string | null> {
45 if (!formatlanacak.delete(yol) || !(await acik($, 'format'))) return null
46 const r = await repoBilgisi($, yol.slice(0, yol.lastIndexOf('/')) || '/')
47 if (!r) return null
48 const b = await formatlayici($, r.dir, yol)
49 if (!b) return null
50 const argv = b.yaz
51 const once = await dosyaOku($, yol)
52 const sonuc = await calistir($, argv, r.dir, 30_000)
53 if (!sonuc.ok) return null
54 if ((await dosyaOku($, yol)) === once) return null
55 return `[genel/format] ${adi(yol)} projenin formatlayıcısıyla (${adi(argv[0]!)}) düzenlendi; aynı dosyayı yeniden düzenlemeden önce oku.`
56}
57hooks/hata.ts 110 lines1import { acik } from './ayar'
2import { notEkle } from './durum'
3import { adi, calistir } from './ortak'
4import type { $T } from './ortak'
5
6const HATA = /(error|ERR!|exception|failed|failure|cannot|can't|could not|not found|denied|refused|EADDRINUSE|ENOENT|EACCES|ECONNREFUSED|fatal|traceback|hata)/i
7
8// Hata satırının değişmeyen en uzun parçası: yol, sayı, tırnak içi ve hex atılır; eski kayıtlarda aynen geçer.
9export function hataImzasi(cikti: string): string | null {
10 const satirlar = cikti.split('\n').map(s => s.trim()).filter(s => HATA.test(s))
11 let en: string | null = null
12 for (const satir of satirlar.slice(0, 20)) {
13 for (const parca of satir.split(/(?:\/[^\s:]+|~\/\S+|0x[0-9a-f]+|\d+|'[^']*'|"[^"]*"|`[^`]*`|\\)/i)) {
14 const p = parca.trim().replace(/^[\s:,.()[\]-]+|[\s:,.()[\]-]+$/g, '')
15 if (p.length >= 18 && p.length <= 160 && HATA.test(p) && (!en || p.length > en.length)) en = p
16 }
17 }
18 return en
19}
20
21type Kayit = { type?: string; timestamp?: string; message?: { content?: unknown } }
22
23function metinBlogu(k: Kayit): string | null {
24 if (k.type !== 'assistant' || !Array.isArray(k.message?.content)) return null
25 const t = (k.message!.content as { type?: string; text?: string }[]).find(b => b.type === 'text' && (b.text ?? '').trim().length > 30)
26 return t?.text?.trim() ?? null
27}
28
29const GENEL_KELIME = new Set(['error', 'errors', 'cannot', 'failed', 'failure', 'could', 'found', 'denied', 'refused', 'fatal', 'while', 'there'])
30
31// İmzadaki ayırt edici kelimeler (ör. "module", "eaddrinuse"); açıklamanın gerçekten bu hatayla ilgili olduğunu bunlar gösterir.
32export function anahtarKelimeler(imza: string): string[] {
33 return [...new Set(imza.toLowerCase().split(/[^a-z0-9_]+/).filter(k => k.length >= 5 && !GENEL_KELIME.has(k)))]
34}
35
36// Kayıt dosyasında hatanın geçtiği satırdan sonra Claude'un hatayı anan ilk açıklaması.
37export function sonrakiAciklama(satirlar: string[], anahtarlar: readonly string[] = []): { zaman: string | null; metin: string | null } {
38 let zaman: string | null = null
39 for (const s of satirlar) {
40 let k: Kayit
41 try {
42 k = JSON.parse(s) as Kayit
43 } catch {
44 continue
45 }
46 zaman ??= k.timestamp ?? null
47 const t = metinBlogu(k)
48 if (t && (anahtarlar.length === 0 || anahtarlar.some(a => t.toLowerCase().includes(a)))) return { zaman, metin: t.replace(/\s+/g, ' ').slice(0, 300) }
49 }
50 return { zaman, metin: null }
51}
52
53const sorulan = new Set<string>()
54let rg: string | null | undefined
55
56// Oturum kayıtları GB'larca tutabilir: ripgrep yarım saniyede tarar, macOS grep'i saniyelerce.
57async function ripgrep($: $T): Promise<string | null> {
58 if (rg === undefined) {
59 rg = null
60 for (const yol of ['/opt/homebrew/bin/rg', '/usr/local/bin/rg', '/usr/bin/rg']) {
61 if (await $.fs.exists(yol)) {
62 rg = yol
63 break
64 }
65 }
66 }
67 return rg
68}
69
70export async function hataHafizasi($: $T, cikti: string): Promise<string | null> {
71 if (!(await acik($, 'hata'))) return null
72 const imza = hataImzasi(cikti)
73 if (!imza || sorulan.has(imza)) return null
74 sorulan.add(imza)
75 if (await ripgrep($)) return hafizadaAra($, imza)
76 // ripgrep yoksa arama komutun sonucunu bekletmez; bulunursa bir sonraki fırsatta iletilir.
77 hafizadaAra($, imza)
78 .then(not => not && notEkle(not))
79 .catch(() => {})
80 return null
81}
82
83async function hafizadaAra($: $T, imza: string): Promise<string | null> {
84 const ev = await $.env.get('HOME')
85 if (!ev) return null
86 const buOturum = await $.session.id()
87 const kok = `${ev}/.claude/projects`
88 const rgYolu = await ripgrep($)
89 const arama = rgYolu
90 ? await calistir($, [rgYolu, '-l', '-F', '--max-depth', '2', '-g', '*.jsonl', '--', imza, kok], undefined, 10_000)
91 : await calistir($, ['find', kok, '-maxdepth', '2', '-name', '*.jsonl', '-exec', 'grep', '-lF', '--', imza, '{}', '+'], undefined, 120_000)
92 const bulunan = arama.out
93 .split('\n')
94 .filter(d => d && !d.endsWith(`/${buOturum}.jsonl`))
95 if (bulunan.length === 0) return null
96
97 const ornekler: string[] = []
98 for (const dosya of bulunan.slice(0, 3)) {
99 const satir = Number((await calistir($, rgYolu ? [rgYolu, '-n', '-m1', '-o', '-F', '--', imza, dosya] : ['grep', '-n', '-m1', '-o', '-F', '--', imza, dosya])).out.split(':')[0])
100 if (!satir) continue
101 const sonra = (await calistir($, ['sed', '-n', `${satir},${satir + 60}p`, dosya])).out.split('\n')
102 const { zaman, metin } = sonrakiAciklama(sonra, anahtarKelimeler(imza))
103 const proje = adi(dosya.slice(0, dosya.lastIndexOf('/'))).replace(/^-Users-[^-]+-(Documents-)?(GitHub-)?/, '')
104 ornekler.push(`- ${proje}${zaman ? `, ${zaman.slice(0, 10)}` : ''}${metin ? `: "${metin}"` : ''} (kayıt: ${dosya}:${satir})`)
105 }
106 if (ornekler.length === 0) return null
107 $.ui.toast(`🧠 Bu hata daha önce ${bulunan.length} oturumda görüldü`, { timeoutMs: 6_000 })
108 return `[genel/hata] "${imza}" hatası eski oturumlarda da görülmüş. Çözüm gerekiyorsa önce bu kayıtlara bak (alıntı varsa o zamanki açıklama):\n${ornekler.join('\n')}`
109}
110hooks/kaldik.tsx 124 lines1import { atom, read, update } from 'claude-code'
2import type { RenderInput } from 'claude-code'
3
4import type { Kaldik } from '../types'
5import { acik } from './ayar'
6import { calistir, kapsamRepolar, projeKlasoru } from './ortak'
7import type { $T } from './ortak'
8
9const kaldik = atom({ plugin: 'genel', key: 'kaldik' } as const, null)
10
11const OZET_MODELI = 'claude-haiku-5-5'
12let baglamVerildi = false
13
14type Satir = { type?: string; timestamp?: string; message?: { content?: unknown } }
15
16// Kayıt kuyruğundan: kullanıcının son istekleri ve Claude'un son cevabı.
17export function kayittanCikar(satirlar: string[]): { zaman: number | null; isteklerim: string[]; sonCevap: string | null } {
18 const isteklerim: string[] = []
19 let sonCevap: string | null = null
20 let zaman: number | null = null
21 for (const s of satirlar) {
22 let k: Satir
23 try {
24 k = JSON.parse(s) as Satir
25 } catch {
26 continue
27 }
28 if (k.timestamp) zaman = Date.parse(k.timestamp)
29 const c = k.message?.content
30 if (k.type === 'user' && typeof c === 'string' && !c.startsWith('<') && !c.startsWith('Another Claude') && c.trim()) {
31 isteklerim.push(c.replace(/\s+/g, ' ').trim().slice(0, 200))
32 }
33 if (k.type === 'assistant' && Array.isArray(c)) {
34 const t = (c as { type?: string; text?: string }[]).filter(b => b.type === 'text' && b.text?.trim()).pop()
35 if (t?.text) sonCevap = t.text.trim()
36 }
37 }
38 return { zaman, isteklerim: isteklerim.slice(-4), sonCevap }
39}
40
41async function oncekiKayit($: $T): Promise<string | null> {
42 const ev = await $.env.get('HOME')
43 if (!ev) return null
44 const klasor = projeKlasoru(ev, await $.session.cwd())
45 const bu = `${await $.session.id()}.jsonl`
46 const dosyalar = (await $.fs.list(klasor).catch(() => []))
47 .filter(f => f.kind === 'file' && f.name.endsWith('.jsonl') && f.name !== bu)
48 .sort((a, b) => b.mtimeMs - a.mtimeMs)
49 return dosyalar[0] ? `${klasor}/${dosyalar[0].name}` : null
50}
51
52export async function kaldikBaslat($: $T) {
53 if (!(await acik($, 'kaldik')) || (await $.session.turns()) > 0) return
54 const dosya = await oncekiKayit($)
55 if (!dosya) return
56 const kuyruk = (await calistir($, ['tail', '-n', '800', dosya])).out.split('\n')
57 const { zaman, isteklerim, sonCevap } = kayittanCikar(kuyruk)
58 if (!zaman || isteklerim.length === 0) return
59
60 const repolar: string[] = []
61 for (const r of await kapsamRepolar($)) {
62 const branch = (await calistir($, ['git', 'branch', '--show-current'], r.dir)).out
63 const degisik = (await calistir($, ['git', 'status', '--porcelain'], r.dir)).out.split('\n').filter(Boolean).length
64 if (branch && (!['main', 'master'].includes(branch) || degisik)) repolar.push(`${r.ad} ${branch}${degisik ? ` (✎${degisik} dosya commit'siz)` : ''}`)
65 }
66
67 const yanit = await $.model.complete({
68 model: OZET_MODELI,
69 maxTokens: 200,
70 system: 'Kısa ve sade Türkçe yaz. Teknik ayrıntı değil, iş durumu.',
71 prompt: `Bir geliştiricinin önceki kodlama oturumunun sonu. İki cümleyle yaz: ne üzerinde çalışılıyordu ve ne yarım kaldı / sıradaki adım ne.\n\nKullanıcının son istekleri:\n${isteklerim.map(i => `- ${i}`).join('\n')}\n\nAsistanın son cevabı:\n${(sonCevap ?? '').slice(0, 2500)}`,
72 })
73 const ozet = yanit.isAnswered ? yanit.text.trim() : (sonCevap ?? '').replace(/\s+/g, ' ').slice(0, 200)
74 await update($, kaldik, () => ({ zaman, ozet, isteklerim, repolar }))
75}
76
77// İlk mesajla Claude'a bir kez: önceki oturumun özeti ("nerede kaldık" sorusu bununla cevaplanır).
78export async function kaldikBaglami($: $T): Promise<string | null> {
79 const k = await read($, kaldik)
80 if (!k || baglamVerildi) return null
81 baglamVerildi = true
82 return `[genel/kaldik] Bu klasördeki önceki oturum (${new Date(k.zaman).toLocaleString('tr-TR')}): ${k.ozet}\nKullanıcının o oturumdaki son istekleri: ${k.isteklerim.map(i => `"${i}"`).join(', ')}${k.repolar.length ? `\nŞu an commit'siz ya da branch'te olan repolar: ${k.repolar.join(', ')}` : ''}`
83}
84
85function ne(zaman: number, simdi: number): string {
86 const d = new Date(zaman)
87 const saat = d.toTimeString().slice(0, 5)
88 const gun = Math.round((new Date(simdi).setHours(0, 0, 0, 0) - new Date(zaman).setHours(0, 0, 0, 0)) / 86_400_000)
89 return gun === 0 ? `bugün ${saat}` : gun === 1 ? `dün ${saat}` : `${d.toLocaleDateString('tr-TR')} ${saat}`
90}
91
92export async function kaldikBant($: $T, e: RenderInput<'AbovePrompt'>) {
93 const k = await read($, kaldik)
94 if (!k || e.props.hasSurvey) return null
95 const { Box, Button, Text } = $.ui.resolve(e)
96 const simdi = await $.clock.now()
97 return (
98 <Box flexDirection="column">
99 <Text wrap="truncate-end">
100 <Text weight="bold">↩ Son oturum ({ne(k.zaman, simdi)}): </Text>
101 <Text>{k.ozet}</Text>
102 </Text>
103 <Box>
104 {k.repolar.length > 0 && <Text dimColor wrap="truncate-end">{k.repolar.join(' · ')} </Text>}
105 <Button
106 key="kaldik-devam"
107 plain
108 label="[devam et]"
109 onPress={async () => {
110 await update($, kaldik, () => null)
111 await $.prompt.submit({ text: 'Önceki oturumda kaldığımız yerden devam edelim.', asUser: true })
112 }}
113 />
114 <Text> </Text>
115 <Button key="kaldik-kapat" plain label="[kapat]" onPress={() => update($, kaldik, () => null)} />
116 </Box>
117 </Box>
118 )
119}
120
121export async function kaldikKapat($: $T) {
122 if (await read($, kaldik)) await update($, kaldik, () => null)
123}
124