SLOPSHOPPER

genel

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

newbandspinnerguardcommandtoast
v0.1.0no licenseupdated 2026-10-20omeraydin00/claude-kurulum/mods/genel
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · genel
› fix the failing auth test and add an audit log call ╭────────────────────────────────────────────╮ │ genel │ ⏺ Read(src/auth.ts) │ 🧪 app: testler kırık, Claude'a geri │ ⎿ Read 6 lines │ gönderildi │ ⏺ 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. ✓ 42 sn · 3 dosya (+16 −1) · 4 komut · 1 hata auth.ts, audit.ts, cache.ts › /genel ⎿ genel: ✓ kaldik Nerede kaldık: projeyi açınca son oturumun özeti ve yarım kalan iş ⎿ genel: ✓ hata Hata hafızası: aynı hata eski oturumlarda çözüldüyse hatırlatır ⎿ genel: ✓ bitti Bitince haber ver: uzun iş bitince bildirim ⎿ genel: ✗ ses Sesli haber: bitince "bitti", beklerken "seni bekliyor" der ⎿ genel: ✓ bekliyor Seni bekliyor: izin ya da cevap beklerken masaüstü bildirimi ⎿ genel: ✓ kart Tur sonu kartı: süre, dosya, satır ve komut sayısı ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ genel: 🖥 sunucu :5003 · web :3000/3001/5173/4200

Draws

Turn line
✓ 42 sn · 3 dosya (+16 −1) · 4 komut · 1 hata auth.ts, audit.ts, cache.ts
README

claude-kurulum

Claude Code kurulumum: her projede çalışan bir mod (genel), birkaç eklenti ve ayar. Yeni bir bilgisayarda tek komutla geri gelir.

Kurulum

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 modu

Her 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çaNe yapar
Nerede kaldıkProjeyi açınca bu klasördeki son oturumun iki cümlelik özeti ve "devam et" düğmesi
Proof of DoneKod 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 formatDü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
Bildirimler1 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 izleyiciPR 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 korumaSı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 cevapClaude 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 durumuAçık geliştirme sunucuları ve telefondan erişilecek LAN adresi

Eklentiler

EklentiNe yapar
claude-hud@claude-hudPrompt 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-officialPR'ı birkaç inceleyiciyle gözden geçirir
cc-safety-net@cc-marketplacerm -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 (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üm

Denenip çıkarılanlar

Ekranı 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.

Geliştirme

claude plugin validate mods/genel
claude plugin test mods/genel
Source 22 files
hooks/register.tsx 147 lines
1import 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}
147
hooks/adim.tsx 66 lines
1import { 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}
66
hooks/ayar.ts 76 lines
1import { 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}
76
hooks/bildirim.ts 38 lines
1import { 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}
38
hooks/bitti.ts 16 lines
1import 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}
16
hooks/ci.tsx 146 lines
1import { 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}
146
hooks/devir.ts 54 lines
1import 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}
54
hooks/durum.ts 25 lines
1import 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}
25
hooks/env.ts 57 lines
1import { 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}
57
hooks/format.ts 57 lines
1import { 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}
57
hooks/hata.ts 110 lines
1import { 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}
110
hooks/kaldik.tsx 124 lines
1import { 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