Skip to content
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.

From plugin
pordee
3302 skills4 hooks
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

Repo: kerlos/pordee