Writing
Hook
Hooks
What pordee runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
> /plugin marketplace add kerlos/pordee > /plugin install pordee@pordee
Ships with pordee. Installing the plugin gets these hooks.
Where it lives
- hooks/pordee-activate.jsGitHub
Read the script
#!/usr/bin/env node // pordee — Claude Code SessionStart activation hook. // Reads state, emits Thai mode reminder via stdout when enabled. // Stdout becomes additionalContext for the session. // Always exits 0 — never blocks session start. try { const { getState } = require('./pordee-config'); const state = getState(); if (!state.enabled) { process.exit(0); } const level = state.level === 'lite' ? 'lite' : 'full'; const ruleset = `PORDEE MODE ACTIVE — level: ${level}\n\n` + 'Respond terse like simple Thai. Keep technical English terms. ' + 'Drop polite particles (ครับ, ค่ะ, นะคะ, นะครับ), hedging (อาจจะ, น่าจะ, จริงๆแล้ว), ' + 'pleasantries (ได้เลยครับ, แน่นอน), and English-style filler (just/really/basically/actually/simply). ' + 'Fragments OK. Use short Thai synonyms (ดู not ตรวจสอบ, แก้ not ทำการแก้ไข, เพราะ not เนื่องจาก).\n\n' + `## Persistence\n\n` + `ACTIVE EVERY RESPONSE. No drift. Off only via "หยุดพอดี", "พูดปกติ", or "/pordee stop".\n\n` + `Current level: **${level}**. Switch: \`/pordee lite|full\`.\n\n` + `## Pattern\n\n` + `\`[ของ] [ทำ] [เหตุผล]. [ขั้นต่อ].\`\n\n` + `## Auto-Clarity\n\n` + `Drop pordee for: security warnings, irreversible actions (DROP TABLE, rm -rf, git push --force, git reset --hard), ` + `multi-step sequences where order matters, user asks "อะไรนะ" / "พูดอีกที" / "อธิบายชัดๆ". ` + `Resume after clarification done.\n\n` + `## Boundaries\n\n` + `Code/commits/PRs/code comments: write normal English. Errors: exact quote. ` + `File paths, URLs, identifiers, function names: exact.`; process.stdout.write(ruleset); process.exit(0); } catch (e) { // Never block session start. Errors logged to file by config helper. process.exit(0); } - hooks/pordee-config.jsGitHub
Read the script
#!/usr/bin/env node // pordee — shared state helper. // State file: $PORDEE_HOME/state.json (defaults to ~/.pordee/state.json). // PORDEE_HOME exists for test isolation. const fs = require('fs'); const path = require('path'); const os = require('os'); const HOME_DIR = process.env.PORDEE_HOME || path.join(os.homedir(), '.pordee'); const STATE_PATH = path.join(HOME_DIR, 'state.json'); const ERROR_LOG_PATH = path.join(HOME_DIR, 'error.log'); const DEFAULT_STATE = Object.freeze({ enabled: false, level: 'full', version: 1 }); const VALID_LEVELS = new Set(['lite', 'full']); function logError(msg) { try { fs.mkdirSync(HOME_DIR, { recursive: true }); fs.appendFileSync(ERROR_LOG_PATH, `[${new Date().toISOString()}] ${msg}\n`); } catch (e) { // Logging is best-effort. } } function getState() { try { if (!fs.existsSync(STATE_PATH)) { return { ...DEFAULT_STATE }; } const raw = fs.readFileSync(STATE_PATH, 'utf8'); const parsed = JSON.parse(raw); return { enabled: typeof parsed.enabled === 'boolean' ? parsed.enabled : DEFAULT_STATE.enabled, level: VALID_LEVELS.has(parsed.level) ? parsed.level : DEFAULT_STATE.level, version: typeof parsed.version === 'number' ? parsed.version : DEFAULT_STATE.version, lastChanged: parsed.lastChanged || undefined }; } catch (e) { logError(`getState: ${e.message}`); return { ...DEFAULT_STATE }; } } function setState(patch) { try { fs.mkdirSync(HOME_DIR, { recursive: true }); const current = getState(); const merged = { ...current, ...patch, version: 1, lastChanged: new Date().toISOString() }; // Validate level after merge. if (!VALID_LEVELS.has(merged.level)) { merged.level = DEFAULT_STATE.level; } const tmpPath = STATE_PATH + '.tmp'; fs.writeFileSync(tmpPath, JSON.stringify(merged, null, 2)); fs.renameSync(tmpPath, STATE_PATH); return merged; } catch (e) { logError(`setState: ${e.message}`); return null; } } module.exports = { STATE_PATH, ERROR_LOG_PATH, DEFAULT_STATE, VALID_LEVELS, getState, setState, logError }; - hooks/pordee-mode-tracker.jsGitHub
Read the script
#!/usr/bin/env node // pordee — UserPromptSubmit hook. // Reads stdin: { prompt, transcript_path?, ... } // Parses prompt for triggers (skipping content inside ``` code fences). // Updates state.json. Emits hookSpecificOutput when pordee enabled. // Always exits 0. const { getState, setState, logError } = require('./pordee-config'); const { execFileSync } = require('child_process'); const path = require('path'); function stripCodeFences(text) { // Remove triple-backtick fenced blocks (multi-line and inline ```...```). return text.replace(/```[\s\S]*?```/g, '').replace(/```[\s\S]*$/, ''); } function parseTrigger(prompt) { const cleaned = stripCodeFences(prompt); const trimmed = cleaned.trim(); // Stats trigger — handled specially by the caller. // Matches /pordee-stats, /pordee:pordee-stats, and --share variant. if (/^\/pordee(?::pordee)?-stats(?:\s+--share)?$/.test(trimmed)) { return { action: 'stats', share: trimmed.includes('--share') }; } // Slash commands — case-insensitive on the command, exact on args. // Matches /pordee, /pordee:pordee, and variants with args (lite/full/stop). const slashMatch = trimmed.match(/^\/pordee(?::pordee)?(?:\s+(\w+))?$/i); if (slashMatch) { const arg = (slashMatch[1] || '').toLowerCase(); if (arg === 'lite') return { enabled: true, level: 'lite' }; if (arg === 'full') return { enabled: true, level: 'full' }; if (arg === 'stop') return { enabled: false }; if (arg === '') return { enabled: true }; // bare /pordee or /pordee:pordee // Unknown subcommand — ignore. return null; } // Thai phrase triggers — match only when the trigger is the entire trimmed input. // Disable triggers checked first so "หยุดพอดี" wins over "พอดี" substring. const enableThai = ['พอดีโหมด', 'พูดสั้นๆ', 'พอดี']; const disableThai = ['หยุดพอดี', 'พูดปกติ']; for (const phrase of disableThai) { if (trimmed === phrase) return { enabled: false }; } for (const phrase of enableThai) { if (trimmed === phrase) return { enabled: true }; } return null; } function emitActiveReminder(state) { process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: `PORDEE MODE ACTIVE (${state.level}). ` + `ตอบไทยกระชับ. Keep technical English terms. ` + `Drop polite particles, hedging, pleasantries. Fragments OK. ` + `Code/commits/security: write normal.` } })); } let input = ''; process.stdin.on('data', chunk => { input += chunk; }); process.stdin.on('end', () => { try { const data = JSON.parse(input); const prompt = (data.prompt || '').toString(); const transcriptPath = data.transcript_path || null; const trigger = parseTrigger(prompt); if (!trigger) { // No trigger — just emit reminder if active const state = getState(); if (state.enabled) emitActiveReminder(state); process.exit(0); } if (trigger.action === 'stats') { // Run pordee-stats.js and return its output as a blocked response const statsScript = path.join(__dirname, 'pordee-stats.js'); const args = ['--session-file', transcriptPath || '']; if (trigger.share) args.push('--share'); let statsOut; try { statsOut = execFileSync(process.execPath, [statsScript, ...args], { encoding: 'utf8', env: process.env, timeout: 5000, }); } catch (statsErr) { statsOut = statsErr.stdout || statsErr.stderr || 'pordee-stats: failed to load stats'; } process.stdout.write(JSON.stringify({ decision: 'block', reason: statsOut.trim(), })); process.exit(0); } // Mode trigger — update state setState(trigger); const state = getState(); if (state.enabled) { emitActiveReminder(state); } } catch (e) { logError(`mode-tracker: ${e.message}`); } process.exit(0); }); - hooks/pordee-stats.jsGitHub
Read the script
#!/usr/bin/env node // pordee-stats — read active Claude Code session log, print token usage + savings. // Based on caveman-stats.js. Uses median compression from benchmarks/compression.json. // // Run directly: node hooks/pordee-stats.js // Inside Claude: /pordee-stats triggers this via UserPromptSubmit hook. const fs = require('fs'); const path = require('path'); const os = require('os'); const { getState } = require('./pordee-config'); // Approximate output-token pricing, USD per million. // Anthropic + Kimi models. Update when pricing changes. const MODEL_OUTPUT_PRICE_PER_M = [ ['claude-opus-4', 75.00], ['claude-sonnet-4', 15.00], ['claude-haiku-4', 4.00], ['claude-3-5-sonnet', 15.00], ['claude-3-5-haiku', 4.00], ['claude-3-opus', 75.00], ['kimi-for-coding', 15.00], ['kimi-k2-5', 15.00], ]; function loadCompression() { const benchmarkDir = process.env.PORDEE_BENCHMARK_DIR || path.join(__dirname, '..', 'benchmarks'); const compressionPath = path.join(benchmarkDir, 'compression.json'); try { const data = JSON.parse(fs.readFileSync(compressionPath, 'utf8')); return data.compression || {}; } catch { return {}; } } function priceForModel(model) { if (!model) return null; for (const [prefix, price] of MODEL_OUTPUT_PRICE_PER_M) { if (model.startsWith(prefix)) return price; } return null; } function formatUsd(amount) { if (amount >= 1) return `$${amount.toFixed(2)}`; if (amount >= 0.01) return `$${amount.toFixed(3)}`; return `$${amount.toFixed(4)}`; } function findRecentSession(claudeDir) { const projectsDir = path.join(claudeDir, 'projects'); let entries; try { entries = fs.readdirSync(projectsDir, { withFileTypes: true }); } catch { return null; } let best = null; const stack = entries.map(e => path.join(projectsDir, e.name)); while (stack.length) { const p = stack.pop(); let st; try { st = fs.statSync(p); } catch { continue; } if (st.isDirectory()) { try { for (const child of fs.readdirSync(p)) stack.push(path.join(p, child)); } catch {} } else if (p.endsWith('.jsonl') && (!best || st.mtimeMs > best.mtime)) { best = { file: p, mtime: st.mtimeMs }; } } return best ? best.file : null; } function parseSession(filePath) { let raw; try { raw = fs.readFileSync(filePath, 'utf8'); } catch { return { outputTokens: 0, cacheReadTokens: 0, turns: 0, model: null }; } let outputTokens = 0; let cacheReadTokens = 0; let turns = 0; let model = null; for (const line of raw.split('\n')) { if (!line.trim()) continue; let entry; try { entry = JSON.parse(line); } catch { continue; } if (entry.type !== 'assistant' || !entry.message) continue; const usage = entry.message.usage; if (!usage) continue; outputTokens += usage.output_tokens || 0; cacheReadTokens += usage.cache_read_input_tokens || 0; turns++; if (!model && entry.message.model) model = entry.message.model; } return { outputTokens, cacheReadTokens, turns, model }; } function deriveSavings({ outputTokens, level, model }) { const compression = loadCompression(); const ratio = compression[level] != null ? compression[level] : null; const price = priceForModel(model); if (ratio === null) return { estSavedTokens: 0, estSavedUsd: 0 }; const estNormal = Math.round(outputTokens / (1 - ratio)); const estSavedTokens = estNormal - outputTokens; const estSavedUsd = price !== null ? (estSavedTokens / 1_000_000) * price : 0; return { estSavedTokens, estSavedUsd }; } function parseDuration(spec) { if (!spec) return null; const m = /^(\d+)([dh])$/.exec(spec.trim()); if (!m) return null; const n = parseInt(m[1], 10); return m[2] === 'd' ? n * 86_400_000 : n * 3_600_000; } function readHistory(historyPath) { try { return fs.readFileSync(historyPath, 'utf8').split('\n').filter(Boolean); } catch { return []; } } function appendHistory(historyPath, line) { try { const dir = path.dirname(historyPath); fs.mkdirSync(dir, { recursive: true }); fs.appendFileSync(historyPath, line + '\n'); } catch { // Best-effort } } function aggregateHistory(historyPath, sinceMs) { const lines = readHistory(historyPath); const cutoff = sinceMs ? Date.now() - sinceMs : null; const latestPerSession = new Map(); for (const line of lines) { let entry; try { entry = JSON.parse(line); } catch { continue; } if (!entry || typeof entry !== 'object') continue; if (cutoff !== null && (entry.ts || 0) < cutoff) continue; const id = entry.session_id || '_'; const prev = latestPerSession.get(id); if (!prev || (entry.ts || 0) >= (prev.ts || 0)) latestPerSession.set(id, entry); } let outputTokens = 0, estSavedTokens = 0, estSavedUsd = 0; for (const e of latestPerSession.values()) { outputTokens += e.output_tokens || 0; estSavedTokens += e.est_saved_tokens || 0; estSavedUsd += e.est_saved_usd || 0; } return { sessions: latestPerSession.size, outputTokens, estSavedTokens, estSavedUsd }; } function humanizeTokens(n) { if (!Number.isFinite(n) || n <= 0) return '0'; if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M'; if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k'; return String(Math.round(n)); } function formatHistory({ sessions, outputTokens, estSavedTokens, estSavedUsd, since }) { const sep = '──────────────────────────────────'; const window = since ? ` (last ${since})` : ''; if (sessions === 0) { return `\nพอดี Stats — สะสม${window}\n${sep}\nยังไม่มี session — รัน /pordee-stats ใน session ใดก็ได้เพื่อเริ่มเก็บข้อมูล\n${sep}\n`; } const usdLine = estSavedUsd > 0 ? `ประหยัด (USD): ~${formatUsd(estSavedUsd)}\n` : ''; return `\nพอดี Stats — สะสม${window}\n${sep}\n` + `Sessions: ${sessions.toLocaleString()}\n${sep}\n` + `Output tokens: ${outputTokens.toLocaleString()}\n` + `ประหยัดโทเค็น: ${estSavedTokens.toLocaleString()}\n` + usdLine +
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withpordee
ทำไมใช้คำเยอะ ตอบสั้นๆ ก็เข้าใจ ได้แรงบันดาลใจมาจาก caveman — pordee เป็นรุ่นภาษาไทยที่ลดเฉพาะ output token โดยไม่เสียความถูกต้องทาง technical pordee คือ Claude Code plugin สั่งให้ agent ตอบภาษาไทยกระชับ — ตัดคำสุภาพ (ครับ/ค่ะ/นะคะ), คำลังเล (อาจจะ/น่าจะ),
Get the whole plugin
Stats
330
Stars
31
Forks
Maintained
Maintenance
JavaScript
Language
MIT
License
3mo ago
Last commit
5mo ago
Created
17h ago
Added

