SLOPSHOPPER

ai-limit-th

ดู limit การใช้ Claude แบบน่ารัก: % ที่ใช้ไปของรอบ 5 ชม. และ 7 วัน เวลารีเซ็ต context ที่เหลือ และเตือนก่อนเต็ม

newpanecommandtoaststatus
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · ai-limit-th
│ ┃ 🐱 AI Limit ✕ › fix the failing auth test and add an audit log call │ ┃ /\_/\ โควตาเหลือเยอะ ลุยเลย! │ ┃ ( ^ω^)🧹✨ ⏺ Read(src/auth.ts) │ ┃ ⎿ Read 6 lines │ ┃ 5 ชั่วโมง ██████░░░░░░░░░░░░░░ 31% ⏺ Update(src/auth.ts) │ ┃ รีเซ็ตแล้ว ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ context ██████████░░░░░░░░░░ 49% ⎿ 3 pass, 1 fail │ ┃ 💸 session $0.420 · วันนี้ $0.420 │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /ailimit │ ⎿ ai-limit-th: /\_/\ ( ^ω^)🧹✨ โควตาเหลือเยอะ ลุยเลย! │ ⎿ ai-limit-th: │ ⎿ ai-limit-th: 5 ชั่วโมง ██████░░░░░░░░░░░░░░ 31% รีเซ็ตแล้ว │ ⎿ ai-limit-th: context ██████████░░░░░░░░░░ 49% ของ 200,000 │ ⎿ ai-limit-th: 💸 session นี้ $0.420 · วันนี้รวม $0.420 (ตั้งงบ: /a │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ ai-limit-th: 😺 5ชม 31%

Draws

Pane · 🐱 AI Limit
/\_/\ โควตาเหลือเยอะ ลุยเลย! ( ^ω^)🧹✨ 5 ชั่วโมง ██████░░░░░░░░░░░░░░ 31% รีเซ็ตแล้ว context ██████████░░░░░░░░░░ 49% 💸 session $0.420 · วันนี้ $0.420
README

Mod 46: AI Limit TH

ดูโควตา Claude แบบน่ารัก: ลิมิตหน้าต่าง 5 ชั่วโมงและ 7 วันจากตัวเลขจริงของ Claude Code พร้อมเวลารีเซ็ต การใช้ context และค่าใช้จ่ายของ session แมวแม่มดน้อยเปลี่ยนหน้าตามโควตาที่เหลือ ย่อในแถบสถานะ และเตือนก่อนเต็ม (ค่าเริ่มต้น 80% และ 95%)

คำสั่ง

คำสั่งทำอะไร
/ailimitดูโควตา + เปิดแผงแมวแม่มด
`/ailimit status on\off`ย่อในแถบสถานะ
/ailimit warn <%> <%>ตั้งเกณฑ์เตือน
/ailimit budget <ดอลลาร์>งบค่าใช้จ่ายต่อวัน รวมทุก session เตือนที่ 80% และเมื่อเกิน / budget off

ติดตั้ง

/plugin install ai-limit-th --marketplace tksoft11/claude-code-mods-th

ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)

โค้ดสอนอะไร

เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. แมวแม่มดตามระดับโควตา
  2. แถบสถานะและการเตือนตอนข้ามเกณฑ์
  3. รับตัวเลขจาก event session.measure
  4. แผงแมวแม่มด
  5. คำสั่ง /ailimit

หมายเหตุ

ลิมิต 5 ชั่วโมง/7 วันมีเฉพาะแพ็กเกจสมาชิก (ใช้ API key จะไม่มี) และจะมีตัวเลขหลังคำตอบแรกของ session ได้แรงบันดาลใจจากแอป PIKORI ของคุณ Ron Thitisak

Source 1 files
hooks/register.tsx 260 lines
1/**
2 * ============================================================================
3 * MOD 46: AI Limit TH - ดูโควตา Claude แบบน่ารัก (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: แอปดูลิมิต AI แบบน่ารัก (ขอบคุณไอเดียจาก PIKORI ของคุณ Ron Thitisak)
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. อ่านลิมิตจริงจาก Claude Code: หน้าต่าง 5 ชั่วโมง และ 7 วัน (เฉพาะแพ็กเกจสมาชิก)
9 *    พร้อมเวลารีเซ็ต, การใช้ context และค่าใช้จ่ายของ session
10 * 2. แมวแม่มดน้อยเปลี่ยนหน้าตามโควตาที่เหลือ: สบายๆ → เริ่มกังวล → ตกใจ
11 * 3. เตือนก่อนเต็ม (ค่าเริ่มต้น 80% และ 95%) และโชว์ย่อในแถบสถานะ
12 * 4. นักบัญชี: รวมค่าใช้จ่ายทุก session ของวันนี้ ตั้งงบรายวันได้ เตือนที่ 80% และเมื่อเกิน
13 *
14 * ข้อมูลมาจาก event session.measure ที่ engine ส่งมาทุกครั้งที่ตัวเลขขยับ
15 * ไม่ต้องยิง API เอง และไม่มีข้อมูลสมมติ
16 */
17
18import type { EngineInterface, Register, SessionMeasureInput } from 'claude-code'
19
20const PANE = 'ai-limit-th'
21const WINDOW_NAME: Record<string, string> = { five_hour: '5 ชั่วโมง', seven_day: '7 วัน', spend_limit: 'วงเงิน' }
22
23let usage: Omit<SessionMeasureInput, 'changed'> | undefined
24let settings: { status: boolean; warnAt: number[]; budget?: number } = { status: true, warnAt: [80, 95] }
25// จำว่าเตือนระดับไหนไปแล้วของหน้าต่างไหน (รีเซ็ตเมื่อหน้าต่างรีเซ็ต)
26let warned: Record<string, number> = {}
27// นักบัญชี: ยอดใช้จ่ายรวมของวันนี้ (ทุก session) และยอดของ session นี้ที่นับไปแล้ว
28let spend = { day: '', usd: 0, warned: 0 }
29let sessionUsd = 0
30
31function today(ms: number): string {
32  const d = new Date(ms)
33  return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`
34}
35
36function money(usd: number): string {
37  return `$${usd.toFixed(usd < 1 ? 3 : 2)}`
38}
39
40// ==========================================================================
41// บทที่ 1: แมวแม่มดตามระดับโควตาที่ใช้ไป
42// ==========================================================================
43function witch(percent: number): { face: string; say: string; color: string } {
44  if (percent >= 95) return { face: '/\\_/\\\n(⊙_⊙;)🧹', say: 'ไม้กวาดจะหมดแรงแล้ว! พักก่อนนะ', color: '#e53935' }
45  if (percent >= 80) return { face: '/\\_/\\\n(・_・;)🧹', say: 'เริ่มเยอะแล้ว ใช้อย่างประหยัดหน่อย', color: '#fb8c00' }
46  if (percent >= 50) return { face: '/\\_/\\\n( •ω•)🧹', say: 'ใช้ไปครึ่งทางแล้ว ยังไหวๆ', color: '#fdd835' }
47  return { face: '/\\_/\\\n( ^ω^)🧹✨', say: 'โควตาเหลือเยอะ ลุยเลย!', color: '#7cb342' }
48}
49
50function bar(percent: number, width = 20): string {
51  const filled = Math.round((Math.min(100, Math.max(0, percent)) / 100) * width)
52  return '█'.repeat(filled) + '░'.repeat(width - filled)
53}
54
55function untilReset(iso: string | undefined, now: number): string {
56  if (!iso) return ''
57  const ms = Date.parse(iso) - now
58  if (!(ms > 0)) return 'รีเซ็ตแล้ว'
59  const m = Math.ceil(ms / 60_000)
60  const d = Math.floor(m / 1440)
61  const h = Math.floor((m % 1440) / 60)
62  return `รีเซ็ตใน ${d ? `${d} วัน ` : ''}${h ? `${h} ชม. ` : ''}${m % 60} นาที`
63}
64
65function windowName(kind: string): string {
66  return WINDOW_NAME[kind] ?? kind
67}
68
69function maxPercent(): number {
70  return Math.max(0, ...(usage?.rateLimits ?? []).map(r => r.percentUsed))
71}
72
73// ==========================================================================
74// บทที่ 2: แถบสถานะย่อ + เตือนตอนข้ามเกณฑ์
75// ==========================================================================
76function updateStatus($: EngineInterface): void {
77  const limits = usage?.rateLimits ?? []
78  const wallet = settings.budget ? `💸 ${money(spend.usd)}/${money(settings.budget)}` : ''
79  if (!settings.status || (limits.length === 0 && !wallet)) {
80    $.ui.status(undefined)
81    return
82  }
83  const icon = maxPercent() >= 95 ? '🙀' : maxPercent() >= 80 ? '😿' : '😺'
84  const parts = limits.map(r => `${r.kind === 'five_hour' ? '5ชม' : r.kind === 'seven_day' ? '7วัน' : r.kind} ${Math.round(r.percentUsed)}%`)
85  $.ui.status([limits.length ? `${icon} ${parts.join(' · ')}` : '', wallet].filter(Boolean).join(' | '))
86}
87
88// ==========================================================================
89// บทที่ 2.5: นักบัญชี - รวมค่าใช้จ่ายทุก session ของวันนี้ แล้วเทียบกับงบที่ตั้งไว้
90// ==========================================================================
91async function account($: EngineInterface, sessionTotal: number): Promise<void> {
92  const delta = Math.max(0, sessionTotal - sessionUsd)
93  sessionUsd = sessionTotal
94  if (delta === 0) return
95  const day = today(await $.clock.now())
96  spend = spend.day === day ? { ...spend, usd: spend.usd + delta } : { day, usd: delta, warned: 0 }
97  if (settings.budget) {
98    const pct = (spend.usd / settings.budget) * 100
99    const level = pct >= 100 ? 100 : pct >= 80 ? 80 : 0
100    if (level > spend.warned) {
101      spend = { ...spend, warned: level }
102      $.ui.toast(level >= 100 ? `💸 ใช้เกินงบวันนี้แล้ว ${money(spend.usd)} / ${money(settings.budget)}` : `💰 ใช้ไป ${Math.round(pct)}% ของงบวันนี้ (${money(spend.usd)} / ${money(settings.budget)})`, { timeoutMs: 12_000 })
103    }
104  }
105  await $.store.set('spend', spend)
106}
107
108function checkWarnings($: EngineInterface): void {
109  for (const r of usage?.rateLimits ?? []) {
110    const key = `${r.kind}@${r.resetsAt ?? ''}`
111    const level = [...settings.warnAt].sort((a, b) => b - a).find(t => r.percentUsed >= t)
112    if (level !== undefined && (warned[key] ?? 0) < level) {
113      warned[key] = level
114      $.ui.toast(`${level >= 95 ? '🙀' : '😿'} ลิมิต ${windowName(r.kind)} ใช้ไป ${r.percentUsed}% แล้ว`, { timeoutMs: 12_000 })
115    }
116  }
117}
118
119const HELP = `🐱 AI Limit TH (Mod 46)
120========================================
121/ailimit                 ดูโควตา + เปิดแผงแมวแม่มด
122/ailimit status on|off   ย่อในแถบสถานะ
123/ailimit warn 70 90      ตั้งเกณฑ์เตือน (%)
124/ailimit budget 5        งบค่าใช้จ่ายต่อวัน (ดอลลาร์) เตือนที่ 80% และเมื่อเกิน | budget off
125ลิมิต 5 ชั่วโมง/7 วันมีเฉพาะแพ็กเกจสมาชิก และจะมีตัวเลขหลังคำตอบแรกของ session`
126
127function summary(now: number): string {
128  if (!usage) return '⏳ ยังไม่มีข้อมูล รอคำตอบแรกของ Claude ก่อนนะ'
129  const lines = [`${witch(maxPercent()).face.replace('\n', ' ')}  ${witch(maxPercent()).say}`, '']
130  if (usage.rateLimits.length === 0) lines.push('ไม่มีข้อมูลลิมิต (ใช้ API key หรือยังไม่ได้รับตัวเลข)')
131  for (const r of usage.rateLimits) lines.push(`${windowName(r.kind).padEnd(10)} ${bar(r.percentUsed)} ${String(r.percentUsed).padStart(5)}%  ${untilReset(r.resetsAt, now)}`)
132  if (usage.context.percent !== undefined) lines.push(`${'context'.padEnd(10)} ${bar(usage.context.percent)} ${String(Math.round(usage.context.percent)).padStart(5)}%  ของ ${usage.context.window.toLocaleString('en-US')} tokens`)
133  if (usage.cost) lines.push(`💸 session นี้ ${money(usage.cost.usd)} · วันนี้รวม ${money(spend.usd)}${settings.budget ? ` / งบ ${money(settings.budget)} (${Math.round((spend.usd / settings.budget) * 100)}%)` : ' (ตั้งงบ: /ailimit budget 5)'}`)
134  return lines.join('\n')
135}
136
137export const register: Register = on => {
138  on('session.start', async ($, e, next) => {
139    settings = { ...settings, ...((await $.store.get('settings')) as typeof settings | undefined) }
140    warned = {}
141    sessionUsd = 0
142    spend = { day: '', usd: 0, warned: 0, ...((await $.store.get('spend')) as typeof spend | undefined) }
143    if (spend.day !== today(await $.clock.now())) spend = { day: today(await $.clock.now()), usd: 0, warned: 0 }
144    await $.command.register({ name: 'ailimit', description: 'ดูโควตา Claude (5 ชม./7 วัน) แบบน่ารัก ค่าใช้จ่ายรายวัน และเตือนก่อนเต็ม (/ailimit help)', argumentHint: 'status on|off | warn <%> <%> | budget <usd>' })
145    return next(e)
146  })
147
148  // ==========================================================================
149  // บทที่ 3: รับตัวเลขจาก engine ทุกครั้งที่ขยับ
150  // ==========================================================================
151  on('session.measure', async ($, e, next) => {
152    usage = { context: e.context, rateLimits: e.rateLimits, cost: e.cost }
153    if (e.cost) await account($, e.cost.usd)
154    checkWarnings($)
155    updateStatus($)
156    $.ui.invalidate('ui.render')
157    return next(e)
158  })
159
160  // ==========================================================================
161  // บทที่ 4: แผงแมวแม่มด
162  // ==========================================================================
163  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
164    const { Box, Text } = $.ui.resolve(e)
165    const now = await $.clock.now()
166    const w = witch(maxPercent())
167    const limits = usage?.rateLimits ?? []
168    return (
169      <Box flexDirection="column">
170        <Box flexDirection="row" gap={2}>
171          <Box flexDirection="column">
172            {w.face.split('\n').map(l => (
173              <Text color="#9575cd">{l}</Text>
174            ))}
175          </Box>
176          <Text color={w.color}>{usage ? w.say : 'รอคำตอบแรกก่อนนะ…'}</Text>
177        </Box>
178        {usage && limits.length === 0 && <Text dimColor>ไม่มีข้อมูลลิมิต (API key หรือยังไม่ได้รับตัวเลข)</Text>}
179        {limits.map(r => (
180          <Box flexDirection="column" marginTop={1}>
181            <Text>
182              {windowName(r.kind)} <Text color={witch(r.percentUsed).color}>{bar(r.percentUsed)}</Text> {r.percentUsed}%
183            </Text>
184            <Text dimColor>{untilReset(r.resetsAt, now)}</Text>
185          </Box>
186        ))}
187        {usage?.context.percent !== undefined && (
188          <Box marginTop={1}>
189            <Text>
190              context <Text color="#4fc3f7">{bar(usage.context.percent)}</Text> {Math.round(usage.context.percent)}%
191            </Text>
192          </Box>
193        )}
194        {usage?.cost && (
195          <Text dimColor>
196            💸 session {money(usage.cost.usd)} · วันนี้ {money(spend.usd)}
197            {settings.budget ? ` / งบ ${money(settings.budget)}` : ''}
198          </Text>
199        )}
200      </Box>
201    )
202  })
203
204  on('command.run', { command: 'ailimit' }, async ($, e) => {
205    const [sub = '', ...rest] = e.args.trim().split(/\s+/).filter(Boolean)
206    switch (sub) {
207      case '': {
208        // ยังไม่มี measurement? ลองถามตรงๆ ก่อน (ฟรี ไม่เสีย token)
209        if (!usage) {
210          const u = await $.session.usage().catch(() => undefined)
211          if (u && (u.rateLimits.length || u.context.percent !== undefined)) usage = { context: u.context, rateLimits: u.rateLimits, cost: u.cost }
212        }
213        await $.ui.open({ id: PANE, title: '🐱 AI Limit' })
214        $.ui.invalidate('ui.render')
215        return { text: summary(await $.clock.now()) }
216      }
217      case 'status':
218        if (rest[0] !== 'on' && rest[0] !== 'off') return { text: 'วิธีใช้: /ailimit status on|off' }
219        settings = { ...settings, status: rest[0] === 'on' }
220        await $.store.set('settings', settings)
221        updateStatus($)
222        return { text: `แถบสถานะ: ${settings.status ? 'แสดง' : 'ซ่อน'}` }
223      case 'budget': {
224        if (rest[0] === 'off') {
225          settings = { ...settings, budget: undefined }
226          await $.store.set('settings', settings)
227          updateStatus($)
228          return { text: 'ยกเลิกงบรายวันแล้ว' }
229        }
230        const usd = Number(rest[0])
231        if (!(usd > 0 && usd <= 10_000)) return { text: `วิธีใช้: /ailimit budget 5  (ดอลลาร์ต่อวัน) | budget off\nวันนี้ใช้ไป ${money(spend.usd)}` }
232        settings = { ...settings, budget: usd }
233        spend = { ...spend, warned: 0 }
234        await $.store.set('settings', settings)
235        updateStatus($)
236        return { text: `💰 งบวันละ ${money(usd)} · วันนี้ใช้ไป ${money(spend.usd)} (${Math.round((spend.usd / usd) * 100)}%)` }
237      }
238      case 'warn': {
239        const levels = rest.map(Number).filter(n => Number.isFinite(n) && n > 0 && n <= 100)
240        if (levels.length === 0 || levels.length !== rest.length) return { text: `วิธีใช้: /ailimit warn 80 95  (ตอนนี้: ${settings.warnAt.join(', ')}%)` }
241        settings = { ...settings, warnAt: levels }
242        await $.store.set('settings', settings)
243        warned = {}
244        return { text: `🔔 เตือนที่ ${levels.join(', ')}%` }
245      }
246      default:
247        return { text: HELP }
248    }
249  })
250}
251
252/**
253 * ========================================================================
254 * การบ้าน / ต่อยอด
255 * ========================================================================
256 * 1. บันทึกเปอร์เซ็นต์ทุกชั่วโมงแล้ววาดกราฟการใช้ย้อนหลัง
257 * 2. ทำนายว่าจะเต็มเมื่อไหร่จากอัตราการใช้ช่วงล่าสุด
258 * 3. ส่งเตือนไปมือถือเมื่อเกิน 95% (ต่อกับ Mod 22)
259 */
260