diff --git a/.agents/hooks/caveman-mode-tracker.js b/.agents/hooks/caveman-mode-tracker.js new file mode 100644 index 0000000..44e1cbd --- /dev/null +++ b/.agents/hooks/caveman-mode-tracker.js @@ -0,0 +1,40 @@ +const { run: runStats } = require("./caveman-stats"); + +function readStdin() { + return new Promise((resolve) => { + let data = ""; + process.stdin.setEncoding("utf8"); + process.stdin.on("data", (chunk) => { + data += chunk; + }); + process.stdin.on("end", () => resolve(data)); + }); +} + +function respond(payload) { + process.stdout.write(`${JSON.stringify(payload)}\n`); +} + +async function main() { + const raw = await readStdin(); + const input = raw.trim() ? JSON.parse(raw) : {}; + const prompt = String(input.prompt || input.userPrompt || "").trim(); + + if (/^\/caveman-stats(?:\s|$)/i.test(prompt)) { + respond({ + decision: "block", + reason: runStats(input), + }); + return; + } + + respond({}); +} + +main().catch((error) => { + respond({ + decision: "block", + reason: `caveman hook failed: ${error.message}`, + }); +}); + diff --git a/.agents/hooks/caveman-stats.js b/.agents/hooks/caveman-stats.js new file mode 100644 index 0000000..8b89fdf --- /dev/null +++ b/.agents/hooks/caveman-stats.js @@ -0,0 +1,119 @@ +const fs = require("fs"); +const path = require("path"); + +const DEFAULT_BASELINE_MULTIPLIER = 2.9; + +function readJsonl(filePath) { + if (!filePath || !fs.existsSync(filePath)) return []; + + return fs + .readFileSync(filePath, "utf8") + .split(/\r?\n/) + .filter(Boolean) + .map((line) => { + try { + return JSON.parse(line); + } catch { + return null; + } + }) + .filter(Boolean); +} + +function collectUsage(entry) { + const candidates = [ + entry.usage, + entry.message && entry.message.usage, + entry.response && entry.response.usage, + entry.result && entry.result.usage, + ].filter(Boolean); + + return candidates.reduce( + (sum, usage) => { + sum.input += Number(usage.input_tokens || usage.inputTokens || usage.prompt_tokens || 0); + sum.output += Number(usage.output_tokens || usage.outputTokens || usage.completion_tokens || 0); + return sum; + }, + { input: 0, output: 0 }, + ); +} + +function compactNumber(value) { + if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}m`; + if (value >= 1_000) return `${(value / 1_000).toFixed(1)}k`; + return String(value); +} + +function formatNumber(value) { + return Math.round(value).toLocaleString("en-US"); +} + +function buildStats(input) { + const transcriptPath = input.transcript_path || input.transcriptPath; + const entries = readJsonl(transcriptPath); + const usage = entries.reduce( + (sum, entry) => { + const item = collectUsage(entry); + sum.input += item.input; + sum.output += item.output; + return sum; + }, + { input: 0, output: 0 }, + ); + + const turns = entries.filter((entry) => entry.type === "assistant" || entry.type === "user").length; + const baselineOutput = Math.round(usage.output * DEFAULT_BASELINE_MULTIPLIER); + const saved = Math.max(0, baselineOutput - usage.output); + const percent = baselineOutput ? Math.round((saved / baselineOutput) * 100) : 0; + + return { + turns, + input: usage.input, + output: usage.output, + baselineOutput, + saved, + percent, + transcriptPath, + }; +} + +function writeLifetimeSuffix(input, stats) { + const cwd = input.cwd || process.cwd(); + const outPath = path.join(cwd, ".agents", "hooks", ".caveman-lifetime-savings"); + + try { + fs.writeFileSync(outPath, ` ${compactNumber(stats.saved)}`, "utf8"); + } catch { + // Stats display should still work if this optional badge write fails. + } +} + +function formatStats(stats) { + if (!stats.transcriptPath) { + return "No transcript path in hook input. Cannot compute real token stats."; + } + + if (!stats.input && !stats.output) { + return [ + "No token usage found in current transcript.", + `Transcript: ${stats.transcriptPath}`, + ].join("\n"); + } + + return [ + `Session: ${formatNumber(stats.turns)} turns`, + `Input: ${formatNumber(stats.input)} tokens`, + `Output: ${formatNumber(stats.output)} tokens (caveman)`, + `Baseline:${formatNumber(stats.baselineOutput).padStart(10)} tokens (estimated without caveman)`, + `Saved: ${formatNumber(stats.saved).padStart(10)} tokens (~${stats.percent}%)`, + ].join("\n"); +} + +function run(input) { + const stats = buildStats(input || {}); + writeLifetimeSuffix(input || {}, stats); + return formatStats(stats); +} + +module.exports = { run, buildStats, formatStats }; + diff --git a/.agents/skills/caveman-compress/scripts/__pycache__/__init__.cpython-314.pyc b/.agents/skills/caveman-compress/scripts/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..99b84ec Binary files /dev/null and b/.agents/skills/caveman-compress/scripts/__pycache__/__init__.cpython-314.pyc differ diff --git a/.agents/skills/caveman-compress/scripts/__pycache__/__main__.cpython-314.pyc b/.agents/skills/caveman-compress/scripts/__pycache__/__main__.cpython-314.pyc new file mode 100644 index 0000000..1fb9300 Binary files /dev/null and b/.agents/skills/caveman-compress/scripts/__pycache__/__main__.cpython-314.pyc differ diff --git a/.agents/skills/caveman-compress/scripts/__pycache__/cli.cpython-314.pyc b/.agents/skills/caveman-compress/scripts/__pycache__/cli.cpython-314.pyc new file mode 100644 index 0000000..710e3ac Binary files /dev/null and b/.agents/skills/caveman-compress/scripts/__pycache__/cli.cpython-314.pyc differ diff --git a/.agents/skills/caveman-compress/scripts/__pycache__/compress.cpython-314.pyc b/.agents/skills/caveman-compress/scripts/__pycache__/compress.cpython-314.pyc new file mode 100644 index 0000000..c416338 Binary files /dev/null and b/.agents/skills/caveman-compress/scripts/__pycache__/compress.cpython-314.pyc differ diff --git a/.agents/skills/caveman-compress/scripts/__pycache__/detect.cpython-314.pyc b/.agents/skills/caveman-compress/scripts/__pycache__/detect.cpython-314.pyc new file mode 100644 index 0000000..a812271 Binary files /dev/null and b/.agents/skills/caveman-compress/scripts/__pycache__/detect.cpython-314.pyc differ diff --git a/.agents/skills/caveman-compress/scripts/__pycache__/validate.cpython-314.pyc b/.agents/skills/caveman-compress/scripts/__pycache__/validate.cpython-314.pyc new file mode 100644 index 0000000..c1cc61b Binary files /dev/null and b/.agents/skills/caveman-compress/scripts/__pycache__/validate.cpython-314.pyc differ diff --git a/.claude/settings.json b/.claude/settings.json index c49106c..ce7b37d 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -2,6 +2,18 @@ "env": { "ECC_DISABLED_HOOKS": "pre:edit-write:gateguard-fact-force,pre:bash:gateguard-fact-force" }, + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "node .agents/hooks/caveman-mode-tracker.js" + } + ] + } + ] + }, "permissions": { "allow": [ "Bash(docker --version)", @@ -73,9 +85,6 @@ "Bash(python assets/scripts/search.py \"steel industry admin dashboard CMS analytics RTL\" --design-system -p \"Foolad Panel\")", "Bash(Get-ChildItem -Path \"c:\\\\Users\\\\DATIS STAR\\\\پنل\\\\src\\\\app\\\\api\\\\\" -Recurse -File)", "Read(//c/Users/DATIS STAR/**)", -<<<<<<< HEAD - "Bash(git branch *)" -======= "Bash(git branch *)", "PowerShell(\\(Get-Item \"c:\\\\Users\\\\DATIS STAR\\\\andishkade-foolad\\\\public\\\\logo.png\"\\).Length)", "Bash(Get-ChildItem -Path \"c:\\\\Users\\\\DATIS STAR\\\\andishkade-foolad\" -Force)", @@ -90,7 +99,6 @@ "Bash(huggingface-cli version *)", "Bash(hf auth *)", "Bash(hf upload *)" ->>>>>>> space/main ], "additionalDirectories": [ "c:\\Users\\DATIS STAR\\پنل" diff --git a/CLAUDE.md b/CLAUDE.md index 8c0bd92..269326a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,6 +44,7 @@ The panel uses Postgres (`pg` Pool over `DATABASE_URL`) behind a better-sqlite3- - **Globe** (`src/components/ui/globe.tsx`): three-globe mounted in react-three-fiber via `` (NOT via `extend`/JSX-element augmentation — that augmentation poisons global JSX types and breaks unrelated components like `React.ElementType` icons). Country polygons load at runtime from `public/globe-countries.geojson` (bundled, public-domain Natural Earth). - **Calendar** (`src/components/ui/EventCalendar.tsx`): a native Jalali (Persian) month grid built with `Intl.DateTimeFormat('en-US-u-ca-persian', …)` — **no date library**. It walks Gregorian `Date`s and reads their Persian parts to lay out the grid. - **Content layer**: `src/content/*.ts` and inline arrays hold all section data. These are mock/seed data and are explicitly intended to be replaced by `fetch` calls to the panel API later (see "Wiring", below). Keep new editorial data in this shape so the swap stays mechanical. +- **Generator shape**: editorial automation must treat `در یک نگاه` and `رادار آینده` as separate products. `در یک نگاه` is short card copy. `رادار آینده` is long-form analysis with multiple inputs, in-text citations, intro/build-up/conclusion, and article blocks that map to `/radar/post/:id`. ## Admin panel architecture (`panel/`) @@ -194,4 +195,4 @@ rtk init --global # Add RTK to ~/.claude/CLAUDE.md | Network | curl, wget | 65-70% | Overall average: **60-90% token reduction** on common development operations. - \ No newline at end of file + diff --git a/frontend/src/pages/RadarPost/RadarPost.tsx b/frontend/src/pages/RadarPost/RadarPost.tsx index fad9bae..753699f 100644 --- a/frontend/src/pages/RadarPost/RadarPost.tsx +++ b/frontend/src/pages/RadarPost/RadarPost.tsx @@ -39,7 +39,7 @@ function Sources({ sources, lang }: { sources: { title: string; url: string }[];