diff --git a/.gitignore b/.gitignore index edc9aaa..f87f2f7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,8 @@ __pycache__/ .venv/ venv/ -# OpenWolf runtime -.wolf/daemon.log -.wolf/designqc-captures/ +# OpenWolf (all of it — local-only state) +.wolf/ # OS .DS_Store diff --git a/.wolf/OPENWOLF.md b/.wolf/OPENWOLF.md deleted file mode 100644 index aae2a47..0000000 --- a/.wolf/OPENWOLF.md +++ /dev/null @@ -1,135 +0,0 @@ -# OpenWolf Operating Protocol - -You are working in an OpenWolf-managed project. These rules apply every turn. - -## File Navigation - -1. Check `.wolf/anatomy.md` BEFORE reading any file. It has a 2-3 line description and token estimate for every file in the project. -2. If the description in anatomy.md is sufficient for your task, do NOT read the full file. -3. If a file is not in anatomy.md, search with Grep/Glob, then update anatomy.md with the new entry. - -## Code Generation - -1. Before generating code, read `.wolf/cerebrum.md` and respect every entry. -2. Check the `## Do-Not-Repeat` section — these are past mistakes that must not recur. -3. Follow all conventions in `## Key Learnings` and `## User Preferences`. - -## After Actions - -1. After every significant action, append a one-line entry to `.wolf/memory.md`: - `| HH:MM | description | file(s) | outcome | ~tokens |` -2. After creating, deleting, or renaming files: update `.wolf/anatomy.md`. - -## Cerebrum Learning (MANDATORY — every session) - -OpenWolf's value comes from learning across sessions. You MUST update `.wolf/cerebrum.md` whenever you learn something useful. This is not optional. - -**Update `## User Preferences` when the user:** -- Corrects your approach ("no, do it this way instead") -- Expresses a style preference (naming, structure, formatting) -- Shows a preferred workflow or tool choice -- Rejects a suggestion — record what they preferred instead -- Asks for more/less detail, verbosity, explanation - -**Update `## Key Learnings` when you discover:** -- A project convention not obvious from the code (e.g., "tests go in __tests__/ not test/") -- A framework-specific pattern this project uses -- An API behavior that surprised you -- A dependency quirk or version constraint -- How modules connect or data flows through the system - -**Update `## Do-Not-Repeat` (with date) when:** -- The user corrects a mistake you made -- You try something that fails and find the right approach -- You discover a gotcha that would trip up a fresh session - -**Update `## Decision Log` when:** -- A significant architectural or technical choice is made -- The user explains why they chose approach A over B -- A trade-off is explicitly discussed - -**The bar is LOW.** If in doubt, add it. A cerebrum entry that's slightly redundant costs nothing. A missing entry means the next session repeats the same discovery process. - -## Bug Logging (MANDATORY) - -**Log a bug to `.wolf/buglog.json` whenever ANY of these happen:** -- The user reports an error, bug, or problem -- A test fails or a command produces an error -- You fix something that was broken -- You edit a file more than twice to get it right -- An import, module, or dependency is missing or wrong -- A runtime error, type error, or syntax error occurs -- A build or lint command fails -- A feature doesn't work as expected -- You change error handling, try/catch blocks, or validation logic -- The user says something "doesn't work", "is broken", or "shows wrong X" - -**Before fixing:** Read `.wolf/buglog.json` first — the fix may already be known. - -**After fixing:** ALWAYS append to `.wolf/buglog.json` with this structure: -```json -{ - "id": "bug-NNN", - "timestamp": "ISO date", - "error_message": "exact error or user complaint", - "file": "file that was fixed", - "root_cause": "why it broke", - "fix": "what you changed to fix it", - "tags": ["relevant", "keywords"], - "related_bugs": [], - "occurrences": 1, - "last_seen": "ISO date" -} -``` - -**The threshold is LOW.** When in doubt, log it. A false positive in the bug log costs nothing. A missed bug means repeating the same mistake later. - -## Token Discipline - -- Never re-read a file already read this session unless it was modified since. -- Prefer anatomy.md descriptions over full file reads when possible. -- Prefer targeted Grep over full file reads when searching for specific code. -- If appending to a file, do not read the entire file first. - -## Design QC - -When the user asks you to check, evaluate, or improve the design/UI of their app: - -1. Run `openwolf designqc` via Bash to capture screenshots. - - The command auto-detects a running dev server, or starts one from package.json if needed - - Use `--url ` only if auto-detection fails - - The command saves compressed JPEG screenshots to `.wolf/designqc-captures/` - - Full pages are captured as sectioned viewport-height images (top, section2, ..., bottom) -2. Read the captured screenshot images from `.wolf/designqc-captures/` using the Read tool. -3. Evaluate the design against modern standards (Shadcn UI, Tailwind, clean React patterns): - - Spacing and whitespace consistency - - Typography hierarchy and readability - - Color contrast and accessibility (WCAG) - - Visual hierarchy and focal points - - Component consistency - - Whether the design looks "dull" or "white-coded" (generic, no personality) -4. Provide specific, actionable feedback with fix suggestions. -5. If the user approves, implement the fixes directly in their code. -6. After fixes, re-run `openwolf designqc` to capture new screenshots and verify improvement. - -**Token awareness:** Each screenshot costs ~2500 tokens. The command compresses images (JPEG quality 70, max width 1200px) to minimize cost. For large apps, use `--routes / /specific-page` to limit captures. - -## Reframe — UI Framework Selection - -When the user asks to change, pick, migrate, or "reframe" their project's UI framework: - -1. Read `.wolf/reframe-frameworks.md` for the full framework knowledge base. -2. Ask the user the decision questions from the file (current stack, priority, Tailwind usage, theme preference, app type). Stop early once the choice narrows to 1-2 options. -3. Present a recommendation with reasoning based on the comparison matrix. -4. Once the user confirms, use the selected framework's prompt from the file — **adapted to the actual project** using `.wolf/anatomy.md` for real file paths, routes, and components. -5. Execute the migration: install dependencies, update config, refactor components. -6. After migration, run `openwolf designqc` to verify the new look. - -**Do NOT read the entire reframe-frameworks.md into context upfront.** Read the decision questions and comparison matrix first (~50 lines). Only read the specific framework's prompt section after the user chooses. - -## Session End - -Before ending or when asked to wrap up: - -1. Write a session summary to `.wolf/memory.md`. -2. Review the session: did you learn anything? Did the user correct you? Did you fix a bug? If yes, update `.wolf/cerebrum.md` and/or `.wolf/buglog.json`. diff --git a/.wolf/anatomy.md b/.wolf/anatomy.md deleted file mode 100644 index 25f15d5..0000000 --- a/.wolf/anatomy.md +++ /dev/null @@ -1,25 +0,0 @@ -# anatomy.md - -> Auto-maintained by OpenWolf. Last scanned: 2026-06-03T13:57:12.941Z -> Files: 9 tracked | Anatomy hits: 0 | Misses: 0 - -## ../../.claude/plans/ - -- `in-the-example-folder-immutable-rossum.md` — Plan: PDF → static HTML/CSS site (poppler-utils) (~2113 tok) - -## ./ - -- `.gitignore` — Git ignore rules (~55 tok) -- `.python-version` (~2 tok) -- `CLAUDE.md` — CLAUDE.md (~550 tok) -- `main.py` — Convert every PDF in examples/ into a static HTML/CSS site under output/. (~2847 tok) -- `pyproject.toml` — Python project configuration (~44 tok) -- `README.md` — Project documentation (~909 tok) - -## .claude/ - -- `settings.json` (~441 tok) - -## .claude/rules/ - -- `openwolf.md` (~313 tok) diff --git a/.wolf/buglog.json b/.wolf/buglog.json deleted file mode 100644 index a11dde9..0000000 --- a/.wolf/buglog.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "version": 1, - "bugs": [] -} diff --git a/.wolf/cerebrum.md b/.wolf/cerebrum.md deleted file mode 100644 index 13ee75f..0000000 --- a/.wolf/cerebrum.md +++ /dev/null @@ -1,25 +0,0 @@ -# Cerebrum - -> OpenWolf's learning memory. Updated automatically as the AI learns from interactions. -> Do not edit manually unless correcting an error. -> Last updated: 2026-06-03 - -## User Preferences - - - -## Key Learnings - -- **Project:** netgazet — converts NETgazet print PDFs (InDesign-laid-out, ~A3, 16pp) into a static HTML/CSS site under `output/`. Entry point is `main.py`; runs via `python main.py` or `uv run python main.py`. Inputs live in `examples/`. -- **Conversion approach:** page-as-JPEG via `pdftocairo -jpeg -singlefile -r 150` + responsive `img { max-width: 100% }`. `pdftohtml` was deliberately rejected because its complex-mode output is fixed-pixel and not mobile-responsive. Selectable text is provided as a `
` block per page from `pdftotext -layout`, split on form-feed (`\f`). -- **`pdftocairo` filename quirk:** without `-singlefile` it appends `-.jpg` to the output stem; with `-singlefile` it appends just `.jpg`. The script uses `-singlefile` per page so file paths are predictable. -- **No third-party Python deps.** Conversion shells out via `subprocess.run([...], check=True)`; `pyproject.toml` keeps `dependencies = []`. - -## Do-Not-Repeat - - - - -## Decision Log - - diff --git a/.wolf/config.json b/.wolf/config.json deleted file mode 100644 index d2c76ae..0000000 --- a/.wolf/config.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "version": 1, - "openwolf": { - "enabled": true, - "anatomy": { - "auto_scan_on_init": true, - "rescan_interval_hours": 6, - "max_description_length": 100, - "max_files": 500, - "exclude_patterns": [ - "node_modules", - ".git", - "dist", - "build", - ".wolf", - ".next", - ".nuxt", - "coverage", - "__pycache__", - ".cache", - "target", - ".vscode", - ".idea", - ".turbo", - ".vercel", - ".netlify", - ".output", - "*.min.js", - "*.min.css" - ] - }, - "token_audit": { - "enabled": true, - "report_frequency": "weekly", - "waste_threshold_percent": 15, - "chars_per_token_code": 3.5, - "chars_per_token_prose": 4.0 - }, - "cron": { - "enabled": true, - "max_retry_attempts": 3, - "dead_letter_enabled": true, - "heartbeat_interval_minutes": 30, - "use_claude_p": true, - "api_key_env": null - }, - "memory": { - "consolidation_after_days": 7, - "max_entries_before_consolidation": 200 - }, - "cerebrum": { - "max_tokens": 2000, - "reflection_frequency": "weekly" - }, - "daemon": { - "port": 18790, - "log_level": "info" - }, - "dashboard": { - "enabled": true, - "port": 18791 - }, - "designqc": { - "enabled": true, - "viewports": [ - { "name": "desktop", "width": 1440, "height": 900 }, - { "name": "mobile", "width": 375, "height": 812 } - ], - "max_screenshots": 6, - "chrome_path": null - } - } -} diff --git a/.wolf/cron-manifest.json b/.wolf/cron-manifest.json deleted file mode 100644 index 7e2d7f3..0000000 --- a/.wolf/cron-manifest.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "version": 1, - "tasks": [ - { - "id": "anatomy-rescan", - "name": "Full anatomy rescan", - "schedule": "0 */6 * * *", - "description": "Re-scans project filesystem and reconciles anatomy.md", - "action": { "type": "scan_project" }, - "retry": { - "max_attempts": 3, - "backoff": "exponential", - "base_delay_seconds": 30 - }, - "failsafe": { - "on_failure": "log_and_continue", - "alert_after_consecutive_failures": 2, - "dead_letter": true - }, - "enabled": true - }, - { - "id": "memory-consolidation", - "name": "Consolidate old memory", - "schedule": "0 2 * * *", - "description": "Compress memory.md entries older than 7 days", - "action": { - "type": "consolidate_memory", - "params": { "older_than_days": 7 } - }, - "retry": { - "max_attempts": 2, - "backoff": "exponential", - "base_delay_seconds": 60 - }, - "failsafe": { - "on_failure": "skip_and_retry_next_cycle", - "dead_letter": false - }, - "enabled": true - }, - { - "id": "token-audit", - "name": "Token audit report", - "schedule": "0 0 * * 1", - "description": "Weekly waste pattern detection", - "action": { "type": "generate_token_report" }, - "retry": { - "max_attempts": 2, - "backoff": "linear", - "base_delay_seconds": 60 - }, - "failsafe": { "on_failure": "log_and_continue", "dead_letter": true }, - "enabled": true - }, - { - "id": "cerebrum-reflection", - "name": "Cerebrum reflection", - "schedule": "0 3 * * 0", - "description": "Weekly AI review of cerebrum.md — prune stale entries, consolidate duplicates", - "action": { - "type": "ai_task", - "params": { - "prompt": "Review this cerebrum.md. Remove duplicate preferences (keep newer). Remove Do-Not-Repeat entries older than 90 days if no longer relevant. Consolidate Key Learnings that overlap. Keep the file under 2000 tokens. Return the cleaned file content only.", - "context_files": [".wolf/cerebrum.md"] - } - }, - "retry": { - "max_attempts": 1, - "backoff": "none", - "base_delay_seconds": 0 - }, - "failsafe": { "on_failure": "log_and_continue", "dead_letter": false }, - "enabled": true - }, - { - "id": "project-suggestions", - "name": "AI suggestions", - "schedule": "0 4 * * 1", - "description": "Weekly AI analysis with project improvement suggestions", - "action": { - "type": "ai_task", - "params": { - "prompt": "Based on the recent memory entries and current project structure, provide: 1) Key achievements this week, 2) Code improvements to consider, 3) Logical next tasks, 4) Technical debt or risks. Be specific and actionable. Return as JSON: {achievements:[], improvements:[], next_tasks:[], risks:[]}", - "context_files": [".wolf/memory.md", ".wolf/anatomy.md"] - } - }, - "retry": { - "max_attempts": 1, - "backoff": "none", - "base_delay_seconds": 0 - }, - "failsafe": { "on_failure": "log_and_continue", "dead_letter": false }, - "enabled": true - } - ] -} diff --git a/.wolf/cron-state.json b/.wolf/cron-state.json deleted file mode 100644 index d85cc62..0000000 --- a/.wolf/cron-state.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "last_heartbeat": "2026-06-03T13:15:40.431Z", - "engine_status": "running", - "execution_log": [], - "dead_letter_queue": [], - "upcoming": [] -} \ No newline at end of file diff --git a/.wolf/designqc-report.json b/.wolf/designqc-report.json deleted file mode 100644 index 8f658ee..0000000 --- a/.wolf/designqc-report.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "captured_at": null, - "captures": [], - "total_size_kb": 0, - "estimated_tokens": 0 -} \ No newline at end of file diff --git a/.wolf/hooks/_session.json b/.wolf/hooks/_session.json deleted file mode 100644 index d15a4f4..0000000 --- a/.wolf/hooks/_session.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "session_id": "session-2026-06-03-1555", - "started": "2026-06-03T13:55:38.248Z", - "files_read": { - "/home/seppe4/vscode_projects/netgazet/output/index.html": { - "count": 1, - "tokens": 0, - "first_read": "2026-06-03T13:56:13.931Z" - }, - "/home/seppe4/vscode_projects/netgazet/.gitignore": { - "count": 1, - "tokens": 0, - "first_read": "2026-06-03T13:57:08.826Z" - } - }, - "files_written": [ - { - "file": "/home/seppe4/vscode_projects/netgazet/README.md", - "action": "create", - "tokens": 1039, - "at": "2026-06-03T13:57:02.392Z" - }, - { - "file": "/home/seppe4/vscode_projects/netgazet/.gitignore", - "action": "create", - "tokens": 59, - "at": "2026-06-03T13:57:12.946Z" - } - ], - "edit_counts": { - "README.md": 1, - ".gitignore": 1 - }, - "anatomy_hits": 0, - "anatomy_misses": 2, - "repeated_reads_warned": 0, - "cerebrum_warnings": 0, - "stop_count": 1 -} \ No newline at end of file diff --git a/.wolf/hooks/package.json b/.wolf/hooks/package.json deleted file mode 100644 index 3dbc1ca..0000000 --- a/.wolf/hooks/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} diff --git a/.wolf/hooks/post-read.js b/.wolf/hooks/post-read.js deleted file mode 100644 index 5d22fbb..0000000 --- a/.wolf/hooks/post-read.js +++ /dev/null @@ -1,69 +0,0 @@ -import * as path from "node:path"; -import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, estimateTokens, readStdin, normalizePath } from "./shared.js"; -async function main() { - ensureWolfDir(); - const wolfDir = getWolfDir(); - const hooksDir = path.join(wolfDir, "hooks"); - const sessionFile = path.join(hooksDir, "_session.json"); - const raw = await readStdin(); - let input; - try { - input = JSON.parse(raw); - } - catch { - process.exit(0); - return; - } - const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? ""; - const content = input.tool_output?.content ?? ""; - if (!filePath) { - process.exit(0); - return; - } - const normalizedFile = normalizePath(filePath); - // Skip tracking for .wolf/ internal files — consistent with pre-read - const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd()); - const relToProject = normalizedFile.startsWith(projectDir) - ? normalizedFile.slice(projectDir.length).replace(/^\//, "") - : ""; - if (relToProject.startsWith(".wolf/") || relToProject.startsWith(".wolf\\")) { - process.exit(0); - return; - } - const ext = path.extname(filePath).toLowerCase(); - const codeExts = new Set([".ts", ".js", ".tsx", ".jsx", ".py", ".rs", ".go", ".java", ".c", ".cpp", ".css", ".json", ".yaml", ".yml"]); - const proseExts = new Set([".md", ".txt", ".rst"]); - const type = codeExts.has(ext) ? "code" : proseExts.has(ext) ? "prose" : "mixed"; - let tokens = content ? estimateTokens(content, type) : 0; - // Fallback: if tool_output had no content, use anatomy token estimate - if (tokens === 0) { - const anatomyContent = readMarkdown(path.join(wolfDir, "anatomy.md")); - const sections = parseAnatomy(anatomyContent); - for (const [sectionKey, entries] of sections) { - for (const entry of entries) { - const entryRelPath = normalizePath(path.join(sectionKey, entry.file)); - if (normalizedFile.endsWith(entryRelPath) || normalizedFile.endsWith("/" + entryRelPath)) { - tokens = entry.tokens; - break; - } - } - if (tokens > 0) - break; - } - } - const session = readJSON(sessionFile, { files_read: {} }); - if (session.files_read[normalizedFile]) { - session.files_read[normalizedFile].tokens = tokens; - } - else { - session.files_read[normalizedFile] = { - count: 1, - tokens, - first_read: new Date().toISOString(), - }; - } - writeJSON(sessionFile, session); - process.exit(0); -} -main().catch(() => process.exit(0)); -//# sourceMappingURL=post-read.js.map \ No newline at end of file diff --git a/.wolf/hooks/post-write.js b/.wolf/hooks/post-write.js deleted file mode 100644 index 9a5ce97..0000000 --- a/.wolf/hooks/post-write.js +++ /dev/null @@ -1,503 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import * as crypto from "node:crypto"; -import { getWolfDir, ensureWolfDir, readJSON, writeJSON, parseAnatomy, serializeAnatomy, extractDescription, estimateTokens, appendMarkdown, timeShort, readStdin, normalizePath } from "./shared.js"; -async function main() { - ensureWolfDir(); - const wolfDir = getWolfDir(); - const hooksDir = path.join(wolfDir, "hooks"); - const sessionFile = path.join(hooksDir, "_session.json"); - const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd(); - const raw = await readStdin(); - let input; - try { - input = JSON.parse(raw); - } - catch { - process.exit(0); - return; - } - const toolName = input.tool_name ?? "Write"; - const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? ""; - if (!filePath) { - process.exit(0); - return; - } - const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath); - // Skip processing for .wolf/ internal files to avoid slow self-referential updates - const relPath = normalizePath(path.relative(projectRoot, absolutePath)); - if (relPath.startsWith(".wolf/")) { - process.exit(0); - return; - } - // Never track .env files in anatomy — they contain secrets - const baseName = path.basename(absolutePath); - if (baseName === ".env" || baseName.startsWith(".env.")) { - process.exit(0); - return; - } - const oldStr = input.tool_input?.old_string ?? ""; - const newStr = input.tool_input?.new_string ?? ""; - // 1. Update anatomy.md - try { - const anatomyPath = path.join(wolfDir, "anatomy.md"); - let anatomyContent; - try { - anatomyContent = fs.readFileSync(anatomyPath, "utf-8"); - } - catch { - anatomyContent = "# anatomy.md\n\n> Auto-maintained by OpenWolf.\n"; - } - const sections = parseAnatomy(anatomyContent); - const relPathLocal = normalizePath(path.relative(projectRoot, absolutePath)); - const dir = path.dirname(relPathLocal); - const fileName = path.basename(relPathLocal); - const sectionKey = dir === "." ? "./" : dir + "/"; - let fileContent = ""; - try { - fileContent = fs.readFileSync(absolutePath, "utf-8"); - } - catch { - fileContent = input.tool_input?.content ?? ""; - } - const desc = extractDescription(absolutePath).slice(0, 100); - const ext = path.extname(absolutePath).toLowerCase(); - const codeExts = new Set([".ts", ".js", ".tsx", ".jsx", ".py", ".json", ".yaml", ".yml", ".css"]); - const proseExts = new Set([".md", ".txt", ".rst"]); - const type = codeExts.has(ext) ? "code" : proseExts.has(ext) ? "prose" : "mixed"; - const tokens = estimateTokens(fileContent, type); - if (!sections.has(sectionKey)) - sections.set(sectionKey, []); - const entries = sections.get(sectionKey); - const idx = entries.findIndex((e) => e.file === fileName); - if (idx !== -1) { - entries[idx] = { file: fileName, description: desc, tokens }; - } - else { - entries.push({ file: fileName, description: desc, tokens }); - } - let fileCount = 0; - for (const [, list] of sections) - fileCount += list.length; - const serialized = serializeAnatomy(sections, { - lastScanned: new Date().toISOString(), - fileCount, - hits: 0, - misses: 0, - }); - const tmp = anatomyPath + "." + crypto.randomBytes(4).toString("hex") + ".tmp"; - try { - fs.writeFileSync(tmp, serialized, "utf-8"); - fs.renameSync(tmp, anatomyPath); - } - catch { - try { - fs.writeFileSync(anatomyPath, serialized, "utf-8"); - } - catch { } - try { - fs.unlinkSync(tmp); - } - catch { } - } - } - catch { } - // 2. Append richer entry to memory.md - try { - const action = toolName === "Write" ? "Created" : toolName === "MultiEdit" ? "Multi-edited" : "Edited"; - const relFile = normalizePath(path.relative(projectRoot, absolutePath)); - const fileContent = input.tool_input?.content ?? ""; - const ext = path.extname(absolutePath).toLowerCase(); - const codeExts = new Set([".ts", ".js", ".tsx", ".jsx", ".py", ".json", ".yaml", ".yml", ".css"]); - const type = codeExts.has(ext) ? "code" : "mixed"; - const writeTokens = estimateTokens(fileContent || newStr, type); - let changeDesc = ""; - if (oldStr && newStr) { - changeDesc = summarizeEdit(oldStr, newStr, baseName); - } - const memoryPath = path.join(wolfDir, "memory.md"); - const outcome = changeDesc || "—"; - appendMarkdown(memoryPath, `| ${timeShort()} | ${action} ${relFile} | ${outcome} | ~${writeTokens} |\n`); - } - catch { } - // 3. Record in session tracker + track edit counts - try { - const session = readJSON(sessionFile, { files_written: [], edit_counts: {} }); - if (!session.edit_counts) - session.edit_counts = {}; - const normalizedFile = normalizePath(filePath); - const action = toolName === "Write" ? "create" : "edit"; - const fileContent = input.tool_input?.content ?? ""; - const tokens = estimateTokens(fileContent || newStr, "code"); - session.files_written.push({ - file: normalizedFile, - action, - tokens, - at: new Date().toISOString(), - }); - const editKey = normalizePath(path.relative(projectRoot, absolutePath)); - session.edit_counts[editKey] = (session.edit_counts[editKey] || 0) + 1; - writeJSON(sessionFile, session); - if (session.edit_counts[editKey] >= 3) { - process.stderr.write(`⚠️ OpenWolf: ${baseName} has been edited ${session.edit_counts[editKey]} times this session. If you're fixing a bug, remember to log it to .wolf/buglog.json.\n`); - } - } - catch { } - // 4. Auto-detect bug-fix patterns and log them - try { - if (oldStr && newStr) { - autoDetectBugFix(wolfDir, absolutePath, projectRoot, oldStr, newStr); - } - } - catch { } - process.exit(0); -} -// ─── Edit Summarizer ───────────────────────────────────────────── -function summarizeEdit(oldStr, newStr, filename) { - const oldLines = oldStr.split("\n"); - const newLines = newStr.split("\n"); - const oldCount = oldLines.length; - const newCount = newLines.length; - const ext = path.extname(filename).toLowerCase(); - // --- Structural fixes --- - if (newStr.includes("try") && newStr.includes("catch") && !oldStr.includes("catch")) { - return "added error handling"; - } - if (newStr.includes("?.") && !oldStr.includes("?.")) - return "added optional chaining"; - if (newStr.includes("?? ") && !oldStr.includes("?? ")) - return "added nullish coalescing"; - // --- Deleted code --- - if (!newStr.trim() || newStr.trim().length < oldStr.trim().length * 0.2) { - return `removed ${oldCount} lines`; - } - // --- Import changes --- - const oldImports = oldLines.filter(l => /^\s*(import|require|use |from )/.test(l)).length; - const newImports = newLines.filter(l => /^\s*(import|require|use |from )/.test(l)).length; - if (newImports > oldImports && Math.abs(newCount - oldCount) <= newImports - oldImports + 1) { - return `added ${newImports - oldImports} import(s)`; - } - // --- Value/string replacement (common bug fix: wrong value) --- - if (oldCount === 1 && newCount === 1) { - const o = oldStr.trim(); - const n = newStr.trim(); - // String literal change - const oStr = o.match(/['"`]([^'"`]+)['"`]/); - const nStr = n.match(/['"`]([^'"`]+)['"`]/); - if (oStr && nStr && oStr[1] !== nStr[1]) { - return `"${oStr[1].slice(0, 25)}" → "${nStr[1].slice(0, 25)}"`; - } - // Number change - const oNum = o.match(/\b(\d+\.?\d*)\b/); - const nNum = n.match(/\b(\d+\.?\d*)\b/); - if (oNum && nNum && oNum[1] !== nNum[1] && o.replace(oNum[1], "") === n.replace(nNum[1], "")) { - return `${oNum[1]} → ${nNum[1]}`; - } - return "inline fix"; - } - // --- Method/function call changes --- - const oldCalls = extractCalls(oldStr); - const newCalls = extractCalls(newStr); - const addedCalls = newCalls.filter(c => !oldCalls.includes(c)); - const removedCalls = oldCalls.filter(c => !newCalls.includes(c)); - if (removedCalls.length === 1 && addedCalls.length === 1) { - return `${removedCalls[0]}() → ${addedCalls[0]}()`; - } - // --- CSS/style changes --- - if (ext === ".css" || ext === ".scss" || ext === ".vue" || ext === ".tsx" || ext === ".jsx") { - const oldProps = (oldStr.match(/[\w-]+\s*:/g) || []).map(p => p.replace(/\s*:/, "")); - const newProps = (newStr.match(/[\w-]+\s*:/g) || []).map(p => p.replace(/\s*:/, "")); - const changed = newProps.filter(p => !oldProps.includes(p)); - if (changed.length > 0 && changed.length <= 3) { - return `CSS: ${changed.join(", ")}`; - } - } - // --- Condition changes --- - const oldConds = (oldStr.match(/if\s*\(([^)]+)\)/g) || []); - const newConds = (newStr.match(/if\s*\(([^)]+)\)/g) || []); - if (newConds.length > oldConds.length) { - return `added ${newConds.length - oldConds.length} condition(s)`; - } - // --- Function modified --- - const fnMatch = newStr.match(/(?:function|def|fn|func|async\s+function)\s+(\w+)/); - if (fnMatch) { - return `modified ${fnMatch[1]}()`; - } - // --- Class/method context --- - const methodMatch = newStr.match(/(?:public|private|protected)?\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*[:{]/); - if (methodMatch) { - return `modified ${methodMatch[1]}()`; - } - // --- Size-based fallback --- - if (newCount > oldCount + 5) - return `expanded (+${newCount - oldCount} lines)`; - if (oldCount > newCount + 5) - return `reduced (-${oldCount - newCount} lines)`; - return `${oldCount}→${newCount} lines`; -} -function extractCalls(code) { - return [...new Set((code.match(/(\w+)\s*\(/g) || []) - .map(m => m.match(/(\w+)/)?.[1] || "") - .filter(n => n.length > 2 && !["if", "for", "while", "switch", "catch", "function", "return", "new", "typeof", "instanceof", "const", "let", "var"].includes(n)))]; -} -// ─── Auto Bug Detection ────────────────────────────────────────── -function autoDetectBugFix(wolfDir, absolutePath, projectRoot, oldStr, newStr) { - const bugLogPath = path.join(wolfDir, "buglog.json"); - const bugLog = readJSON(bugLogPath, { version: 1, bugs: [] }); - const relFile = normalizePath(path.relative(projectRoot, absolutePath)); - const basename = path.basename(absolutePath); - const ext = path.extname(basename).toLowerCase(); - // Detect what kind of fix this is - const detection = detectFixPattern(oldStr, newStr, ext); - if (!detection) - return; - // Check for recent duplicate (same file + same category within 5 min) - const recentDupe = bugLog.bugs.find(b => { - if (path.basename(b.file) !== basename) - return false; - if (!b.tags.includes("auto-detected")) - return false; - if (!b.tags.includes(detection.category)) - return false; - const bugTime = new Date(b.last_seen).getTime(); - return (Date.now() - bugTime) < 5 * 60 * 1000; - }); - if (recentDupe) { - recentDupe.occurrences++; - recentDupe.last_seen = new Date().toISOString(); - // Append additional context - if (detection.context && !recentDupe.fix.includes(detection.context)) { - recentDupe.fix += ` | Also: ${detection.context}`; - } - writeJSON(bugLogPath, bugLog); - return; - } - const nextId = `bug-${String(bugLog.bugs.length + 1).padStart(3, "0")}`; - bugLog.bugs.push({ - id: nextId, - timestamp: new Date().toISOString(), - error_message: detection.summary, - file: relFile, - root_cause: detection.rootCause, - fix: detection.fix, - tags: ["auto-detected", detection.category, ext.replace(".", "") || "unknown"], - related_bugs: [], - occurrences: 1, - last_seen: new Date().toISOString(), - }); - writeJSON(bugLogPath, bugLog); -} -function detectFixPattern(oldStr, newStr, ext) { - const oldLines = oldStr.split("\n"); - const newLines = newStr.split("\n"); - // --- Error handling added --- - if (newStr.includes("catch") && !oldStr.includes("catch")) { - const fn = newStr.match(/(?:function|def|async)\s+(\w+)/)?.[1] || "unknown"; - return { - category: "error-handling", - summary: `Missing error handling in ${path.basename(fn)}`, - rootCause: "Code path had no error handling — exceptions would propagate uncaught", - fix: `Added try/catch block`, - context: extractChangedLines(oldStr, newStr), - }; - } - // --- Null/undefined safety --- - if ((newStr.includes("?.") && !oldStr.includes("?.")) || - (newStr.includes("?? ") && !oldStr.includes("?? ")) || - (/!==?\s*(null|undefined)/.test(newStr) && !/!==?\s*(null|undefined)/.test(oldStr))) { - return { - category: "null-safety", - summary: `Null/undefined access in ${path.basename(path.basename(""))}`, - rootCause: "Property access on potentially null/undefined value", - fix: `Added null safety (optional chaining or null check)`, - context: extractChangedLines(oldStr, newStr), - }; - } - // --- Guard clause / early return added --- - if (/if\s*\([^)]*\)\s*(return|throw|continue|break)/.test(newStr) && - !/if\s*\([^)]*\)\s*(return|throw|continue|break)/.test(oldStr)) { - const condition = newStr.match(/if\s*\(([^)]+)\)/)?.[1]?.trim().slice(0, 60) || "condition"; - return { - category: "guard-clause", - summary: `Missing guard clause`, - rootCause: `No early return/throw for edge case: ${condition}`, - fix: `Added guard clause: if (${condition.slice(0, 40)})`, - }; - } - // --- Wrong value / string fix (very common bug) --- - if (oldLines.length <= 3 && newLines.length <= 3) { - const oldJoined = oldStr.trim(); - const newJoined = newStr.trim(); - // String literal changed - const oStrs = oldJoined.match(/['"`]([^'"`]{2,})['"`]/g) || []; - const nStrs = newJoined.match(/['"`]([^'"`]{2,})['"`]/g) || []; - if (oStrs.length > 0 && nStrs.length > 0) { - for (let i = 0; i < Math.min(oStrs.length, nStrs.length); i++) { - if (oStrs[i] !== nStrs[i]) { - return { - category: "wrong-value", - summary: `Incorrect value in code`, - rootCause: `Had ${oStrs[i].slice(0, 50)}`, - fix: `Changed to ${nStrs[i].slice(0, 50)}`, - }; - } - } - } - // Variable name / method call changed - const oldTokens = tokenizeCode(oldJoined); - const newTokens = tokenizeCode(newJoined); - const changed = []; - for (let i = 0; i < Math.min(oldTokens.length, newTokens.length); i++) { - if (oldTokens[i] !== newTokens[i]) { - changed.push([oldTokens[i], newTokens[i]]); - } - } - if (changed.length === 1 && changed[0][0].length > 2) { - return { - category: "wrong-reference", - summary: `Wrong reference: ${changed[0][0]} should be ${changed[0][1]}`, - rootCause: `Used "${changed[0][0]}" instead of "${changed[0][1]}"`, - fix: `Changed ${changed[0][0]} → ${changed[0][1]}`, - }; - } - } - // --- Logic fix (condition changed) --- - const oldCond = oldStr.match(/if\s*\(([^)]+)\)/)?.[1]; - const newCond = newStr.match(/if\s*\(([^)]+)\)/)?.[1]; - if (oldCond && newCond && oldCond !== newCond && oldLines.length <= 5) { - return { - category: "logic-fix", - summary: `Wrong condition in logic`, - rootCause: `Condition was: if (${oldCond.slice(0, 50)})`, - fix: `Changed to: if (${newCond.slice(0, 50)})`, - }; - } - // --- Operator fix (=== vs ==, > vs >=, etc.) --- - const opChange = findOperatorChange(oldStr, newStr); - if (opChange) { - return { - category: "operator-fix", - summary: `Wrong operator: ${opChange.old} should be ${opChange.new}`, - rootCause: `Used "${opChange.old}" instead of "${opChange.new}"`, - fix: `Changed operator ${opChange.old} → ${opChange.new}`, - }; - } - // --- Missing import/require --- - const oldImports = new Set((oldStr.match(/(?:import|require)\s*\(?['"]([^'"]+)['"]\)?/g) || []).map(m => m)); - const newImports = (newStr.match(/(?:import|require)\s*\(?['"]([^'"]+)['"]\)?/g) || []); - const addedImports = newImports.filter(i => !oldImports.has(i)); - if (addedImports.length > 0 && newLines.length - oldLines.length <= addedImports.length + 2) { - const modules = addedImports.map(i => i.match(/['"]([^'"]+)['"]/)?.[1] || "").filter(Boolean); - return { - category: "missing-import", - summary: `Missing import: ${modules.join(", ")}`, - rootCause: `Module(s) not imported: ${modules.join(", ")}`, - fix: `Added import(s) for ${modules.join(", ")}`, - }; - } - // --- Return value fix --- - const oldReturn = oldStr.match(/return\s+(.+)/)?.[1]?.trim(); - const newReturn = newStr.match(/return\s+(.+)/)?.[1]?.trim(); - if (oldReturn && newReturn && oldReturn !== newReturn && oldLines.length <= 5) { - return { - category: "return-value", - summary: `Wrong return value`, - rootCause: `Was returning: ${oldReturn.slice(0, 50)}`, - fix: `Now returns: ${newReturn.slice(0, 50)}`, - }; - } - // --- Async/await fix --- - if (newStr.includes("await ") && !oldStr.includes("await ")) { - return { - category: "async-fix", - summary: `Missing await`, - rootCause: `Async call without await — returned Promise instead of value`, - fix: `Added await to async call`, - context: extractChangedLines(oldStr, newStr), - }; - } - if (newStr.includes("async ") && !oldStr.includes("async ")) { - return { - category: "async-fix", - summary: `Function not marked async`, - rootCause: `Function uses await but wasn't declared async`, - fix: `Added async modifier`, - }; - } - // --- Type annotation/cast fix --- - if (ext === ".ts" || ext === ".tsx") { - if ((newStr.includes(" as ") && !oldStr.includes(" as ")) || - (newStr.includes(": ") && !oldStr.includes(": ") && oldLines.length <= 3)) { - return { - category: "type-fix", - summary: `Type error`, - rootCause: `Missing or incorrect type annotation`, - fix: `Added type assertion/annotation`, - context: extractChangedLines(oldStr, newStr), - }; - } - } - // --- CSS/style fix --- - if (ext === ".css" || ext === ".scss" || ext === ".vue" || ext === ".tsx" || ext === ".jsx") { - const oldProps = extractCSSProps(oldStr); - const newProps = extractCSSProps(newStr); - const changedProps = [...newProps.entries()].filter(([k, v]) => oldProps.get(k) !== v && oldProps.has(k)); - if (changedProps.length > 0 && changedProps.length <= 3) { - const desc = changedProps.map(([k, v]) => `${k}: ${oldProps.get(k)} → ${v}`).join("; "); - return { - category: "style-fix", - summary: `CSS fix: ${changedProps.map(([k]) => k).join(", ")}`, - rootCause: desc, - fix: `Changed ${desc}`, - }; - } - } - // --- Significant diff (catch-all for substantial edits) --- - const diffRatio = Math.abs(newStr.length - oldStr.length) / Math.max(oldStr.length, 1); - if (diffRatio > 0.3 && oldLines.length >= 3 && newLines.length >= 3) { - // Only log if there's meaningful structural change, not just additions - const removedLines = oldLines.filter(l => l.trim() && !newLines.some(nl => nl.trim() === l.trim())); - if (removedLines.length >= 2) { - return { - category: "refactor", - summary: `Significant refactor of ${path.basename("")}`, - rootCause: `${removedLines.length} lines replaced/restructured`, - fix: `Rewrote ${oldLines.length}→${newLines.length} lines (${removedLines.length} removed)`, - context: removedLines.slice(0, 2).map(l => l.trim().slice(0, 50)).join("; "), - }; - } - } - return null; -} -function extractChangedLines(oldStr, newStr) { - const oldLines = new Set(oldStr.split("\n").map(l => l.trim()).filter(Boolean)); - const newLines = newStr.split("\n").map(l => l.trim()).filter(Boolean); - const added = newLines.filter(l => !oldLines.has(l)); - return added.slice(0, 2).map(l => l.slice(0, 60)).join("; "); -} -function tokenizeCode(code) { - return code.replace(/[^\w$]/g, " ").split(/\s+/).filter(t => t.length > 0); -} -function findOperatorChange(oldStr, newStr) { - const operators = ["===", "!==", "==", "!=", ">=", "<=", ">>", "<<", "&&", "||", "??"]; - for (const op of operators) { - if (oldStr.includes(op) && !newStr.includes(op)) { - for (const op2 of operators) { - if (op2 !== op && newStr.includes(op2) && !oldStr.includes(op2)) { - return { old: op, new: op2 }; - } - } - } - } - return null; -} -function extractCSSProps(code) { - const props = new Map(); - const matches = code.matchAll(/([\w-]+)\s*:\s*([^;}\n]+)/g); - for (const m of matches) { - props.set(m[1].trim(), m[2].trim()); - } - return props; -} -main().catch(() => process.exit(0)); -//# sourceMappingURL=post-write.js.map \ No newline at end of file diff --git a/.wolf/hooks/pre-read.js b/.wolf/hooks/pre-read.js deleted file mode 100644 index 6ba67d0..0000000 --- a/.wolf/hooks/pre-read.js +++ /dev/null @@ -1,80 +0,0 @@ -import * as path from "node:path"; -import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, readStdin, normalizePath } from "./shared.js"; -async function main() { - ensureWolfDir(); - const wolfDir = getWolfDir(); - const hooksDir = path.join(wolfDir, "hooks"); - const sessionFile = path.join(hooksDir, "_session.json"); - const raw = await readStdin(); - let input; - try { - input = JSON.parse(raw); - } - catch { - process.exit(0); - return; - } - const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? ""; - if (!filePath) { - process.exit(0); - return; - } - const normalizedFile = normalizePath(filePath); - // Skip tracking for .wolf/ internal files — they're infrastructure, not project files. - // Counting them inflates anatomy miss rates since .wolf/ is excluded from anatomy scanning. - const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd()); - const relToProject = normalizedFile.startsWith(projectDir) - ? normalizedFile.slice(projectDir.length).replace(/^\//, "") - : ""; - if (relToProject.startsWith(".wolf/") || relToProject.startsWith(".wolf\\")) { - process.exit(0); - return; - } - const session = readJSON(sessionFile, { - session_id: "", files_read: {}, anatomy_hits: 0, anatomy_misses: 0, - repeated_reads_warned: 0, - }); - // Check if already read this session - if (session.files_read[normalizedFile]) { - const prev = session.files_read[normalizedFile]; - process.stderr.write(`⚡ OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${prev.tokens} tokens). Consider using your existing knowledge of this file.\n`); - session.files_read[normalizedFile].count++; - session.repeated_reads_warned++; - writeJSON(sessionFile, session); - process.exit(0); - return; - } - // Check anatomy.md for this file - const anatomyContent = readMarkdown(path.join(wolfDir, "anatomy.md")); - const sections = parseAnatomy(anatomyContent); - let found = false; - for (const [sectionKey, entries] of sections) { - for (const entry of entries) { - // Build the full relative path from the section key + filename for accurate matching - const entryRelPath = normalizePath(path.join(sectionKey, entry.file)); - if (normalizedFile.endsWith(entryRelPath) || normalizedFile.endsWith("/" + entryRelPath)) { - process.stderr.write(`📋 OpenWolf anatomy: ${entry.file} — ${entry.description} (~${entry.tokens} tok)\n`); - found = true; - break; - } - } - if (found) - break; - } - if (found) { - session.anatomy_hits++; - } - else { - session.anatomy_misses++; - } - // Record initial read entry (tokens will be updated in post-read) - session.files_read[normalizedFile] = { - count: 1, - tokens: 0, - first_read: new Date().toISOString(), - }; - writeJSON(sessionFile, session); - process.exit(0); -} -main().catch(() => process.exit(0)); -//# sourceMappingURL=pre-read.js.map \ No newline at end of file diff --git a/.wolf/hooks/pre-write.js b/.wolf/hooks/pre-write.js deleted file mode 100644 index 2e84303..0000000 --- a/.wolf/hooks/pre-write.js +++ /dev/null @@ -1,121 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import { getWolfDir, ensureWolfDir, readJSON, readMarkdown, readStdin } from "./shared.js"; -async function main() { - ensureWolfDir(); - const wolfDir = getWolfDir(); - const raw = await readStdin(); - let input; - try { - input = JSON.parse(raw); - } - catch { - process.exit(0); - return; - } - // For Edit tool, the meaningful content is old_string + new_string - const content = input.tool_input?.content ?? ""; - const oldStr = input.tool_input?.old_string ?? ""; - const newStr = input.tool_input?.new_string ?? ""; - const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? ""; - const allContent = [content, oldStr, newStr].join("\n"); - if (!allContent.trim()) { - process.exit(0); - return; - } - // 1. Cerebrum Do-Not-Repeat check - checkCerebrum(wolfDir, allContent); - // 2. Bug log: search for similar past bugs when editing code - // This fires when Claude is about to edit a file — if the edit looks like a fix - // (changing error handling, modifying catch blocks, etc.), check the bug log - if (filePath && (oldStr || content)) { - checkBugLog(wolfDir, filePath, oldStr, newStr, content); - } - process.exit(0); -} -function checkCerebrum(wolfDir, content) { - const cerebrumContent = readMarkdown(path.join(wolfDir, "cerebrum.md")); - const doNotRepeatSection = cerebrumContent.split("## Do-Not-Repeat")[1]; - if (!doNotRepeatSection) - return; - const entries = doNotRepeatSection.split("## ")[0]; - const lines = entries.split("\n").filter((l) => l.trim().startsWith("[") || l.trim().startsWith("-")); - for (const line of lines) { - const trimmed = line.trim().replace(/^[-*]\s*/, "").replace(/^\[[\d-]+\]\s*/, ""); - if (!trimmed) - continue; - const patterns = []; - const quotedMatches = trimmed.match(/"([^"]+)"/g) || trimmed.match(/'([^']+)'/g) || trimmed.match(/`([^`]+)`/g); - if (quotedMatches) { - for (const qm of quotedMatches) { - patterns.push(qm.replace(/["'`]/g, "")); - } - } - const neverMatch = trimmed.match(/(?:never use|avoid|don't use|do not use)\s+(\w+)/i); - if (neverMatch) - patterns.push(neverMatch[1]); - for (const pattern of patterns) { - try { - const regex = new RegExp(`\\b${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i"); - if (regex.test(content)) { - process.stderr.write(`⚠️ OpenWolf cerebrum warning: "${trimmed}" — check your code before proceeding.\n`); - } - } - catch { } - } - } -} -// Common words that appear in most code — must be excluded from similarity matching -const STOP_WORDS = new Set([ - "error", "function", "return", "const", "this", "that", "with", "from", - "import", "export", "class", "interface", "type", "undefined", "null", - "true", "false", "string", "number", "object", "array", "value", - "file", "path", "name", "data", "response", "request", "result", - "should", "must", "does", "have", "been", "will", "would", "could", - "when", "then", "else", "each", "some", "every", "only", -]); -function checkBugLog(wolfDir, filePath, oldStr, newStr, content) { - const bugLogPath = path.join(wolfDir, "buglog.json"); - if (!fs.existsSync(bugLogPath)) - return; - const bugLog = readJSON(bugLogPath, { version: 1, bugs: [] }); - if (bugLog.bugs.length === 0) - return; - const basename = path.basename(filePath); - // ONLY surface bugs that match the SAME file being edited. - // Cross-file matching is too noisy and risks misdirecting Claude. - const fileMatches = bugLog.bugs.filter(b => { - const bugBasename = path.basename(b.file); - return bugBasename === basename; - }); - if (fileMatches.length === 0) - return; - // Further filter: require tag or error_message overlap with the edit content - const editText = (oldStr + " " + newStr + " " + content).toLowerCase(); - const editTokens = tokenize(editText); - const relevant = fileMatches.filter(bug => { - // Check if any bug tag appears in the edit content - const tagHit = bug.tags.some(t => editText.includes(t.toLowerCase())); - if (tagHit) - return true; - // Check meaningful word overlap (excluding stop words) - const bugTokens = tokenize(bug.error_message + " " + bug.root_cause); - const overlap = [...editTokens].filter(t => bugTokens.has(t)); - // Require at least 3 meaningful overlapping words - return overlap.length >= 3; - }); - if (relevant.length === 0) - return; - // Surface as a FYI, not a directive — Claude should evaluate, not blindly apply - process.stderr.write(`📋 OpenWolf buglog: ${relevant.length} past bug(s) found for ${basename} — review for context, do NOT apply blindly:\n`); - for (const bug of relevant.slice(0, 2)) { - process.stderr.write(` [${bug.id}] "${bug.error_message.slice(0, 70)}"\n Cause: ${bug.root_cause.slice(0, 80)}\n Fix: ${bug.fix.slice(0, 80)}\n`); - } -} -function tokenize(text) { - return new Set(text.replace(/[^\w\s]/g, " ").split(/\s+/) - .filter(w => w.length > 3 && !STOP_WORDS.has(w.toLowerCase())) - .map(w => w.toLowerCase())); -} -main().catch(() => process.exit(0)); -//# sourceMappingURL=pre-write.js.map \ No newline at end of file diff --git a/.wolf/hooks/session-start.js b/.wolf/hooks/session-start.js deleted file mode 100644 index 7da38f3..0000000 --- a/.wolf/hooks/session-start.js +++ /dev/null @@ -1,77 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import { getWolfDir, ensureWolfDir, writeJSON, appendMarkdown, readJSON, timestamp, timeShort } from "./shared.js"; -async function main() { - ensureWolfDir(); - const wolfDir = getWolfDir(); - // Clean up stale .tmp files left from failed atomic writes - try { - const files = fs.readdirSync(wolfDir); - for (const f of files) { - if (f.endsWith(".tmp")) { - try { - fs.unlinkSync(path.join(wolfDir, f)); - } - catch { } - } - } - } - catch { } - const hooksDir = path.join(wolfDir, "hooks"); - const sessionFile = path.join(hooksDir, "_session.json"); - const now = new Date(); - const sessionId = `session-${now.toISOString().slice(0, 10)}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`; - // Create fresh session state - writeJSON(sessionFile, { - session_id: sessionId, - started: timestamp(), - files_read: {}, - files_written: [], - edit_counts: {}, - anatomy_hits: 0, - anatomy_misses: 0, - repeated_reads_warned: 0, - cerebrum_warnings: 0, - stop_count: 0, - }); - // Append session header to memory.md - const memoryPath = path.join(wolfDir, "memory.md"); - const header = `\n## Session: ${now.toISOString().slice(0, 10)} ${timeShort()}\n\n| Time | Action | File(s) | Outcome | ~Tokens |\n|------|--------|---------|---------|--------|\n`; - appendMarkdown(memoryPath, header); - // Check cerebrum freshness — remind Claude to learn - try { - const cerebrumPath = path.join(wolfDir, "cerebrum.md"); - const cerebrumContent = fs.readFileSync(cerebrumPath, "utf-8"); - const stat = fs.statSync(cerebrumPath); - const daysSinceUpdate = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60 * 24); - // Count actual entries (non-comment, non-empty lines in content sections) - const entryLines = cerebrumContent.split("\n").filter(l => { - const t = l.trim(); - return t.startsWith("- ") || t.startsWith("* ") || (t.startsWith("[") && t.includes("]")); - }); - if (entryLines.length < 3) { - process.stderr.write(`💡 OpenWolf: cerebrum.md has only ${entryLines.length} entries. Learn from this session — record user preferences, project conventions, and mistakes to .wolf/cerebrum.md.\n`); - } - else if (daysSinceUpdate > 3) { - process.stderr.write(`💡 OpenWolf: cerebrum.md hasn't been updated in ${Math.floor(daysSinceUpdate)} days. Look for opportunities to add learnings this session.\n`); - } - } - catch { } - // Check buglog — remind if empty - try { - const buglogPath = path.join(wolfDir, "buglog.json"); - const buglog = readJSON(buglogPath, { bugs: [] }); - if (buglog.bugs.length === 0) { - process.stderr.write(`📋 OpenWolf: buglog.json is empty. If you encounter or fix any bugs, errors, or failed tests this session, log them to .wolf/buglog.json.\n`); - } - } - catch { } - // Increment total_sessions in token-ledger - const ledgerPath = path.join(wolfDir, "token-ledger.json"); - const ledger = readJSON(ledgerPath, { version: 1, lifetime: { total_sessions: 0 } }); - ledger.lifetime.total_sessions++; - writeJSON(ledgerPath, ledger); - process.exit(0); -} -main().catch(() => process.exit(0)); -//# sourceMappingURL=session-start.js.map \ No newline at end of file diff --git a/.wolf/hooks/shared.js b/.wolf/hooks/shared.js deleted file mode 100644 index e402d75..0000000 --- a/.wolf/hooks/shared.js +++ /dev/null @@ -1,614 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import * as crypto from "node:crypto"; -export function getWolfDir() { - // Prefer CLAUDE_PROJECT_DIR so hooks work even if CWD changes during a session - const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); - return path.join(projectDir, ".wolf"); -} -/** - * Bail out silently if .wolf/ directory doesn't exist in the current project. - * Call this at the top of every hook to avoid crashes in non-OpenWolf projects. - */ -export function ensureWolfDir() { - const wolfDir = getWolfDir(); - if (!fs.existsSync(wolfDir)) { - process.exit(0); - } -} -export function readJSON(filePath, fallback) { - try { - return JSON.parse(fs.readFileSync(filePath, "utf-8")); - } - catch { - return fallback; - } -} -export function writeJSON(filePath, data) { - const dir = path.dirname(filePath); - if (!fs.existsSync(dir)) - fs.mkdirSync(dir, { recursive: true }); - const tmp = filePath + "." + crypto.randomBytes(4).toString("hex") + ".tmp"; - try { - fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8"); - fs.renameSync(tmp, filePath); - } - catch { - // On Windows, rename can fail if another process holds a handle. - // Fall back to direct write and clean up the tmp file. - try { - fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8"); - } - catch { } - try { - fs.unlinkSync(tmp); - } - catch { } - } -} -export function readMarkdown(filePath) { - try { - return fs.readFileSync(filePath, "utf-8"); - } - catch { - return ""; - } -} -export function appendMarkdown(filePath, line) { - const dir = path.dirname(filePath); - if (!fs.existsSync(dir)) - fs.mkdirSync(dir, { recursive: true }); - fs.appendFileSync(filePath, line, "utf-8"); -} -export function parseAnatomy(content) { - const sections = new Map(); - let currentSection = ""; - for (const line of content.split("\n")) { - const sm = line.match(/^## (.+)/); - if (sm) { - currentSection = sm[1].trim(); - if (!sections.has(currentSection)) - sections.set(currentSection, []); - continue; - } - if (!currentSection) - continue; - const em = line.match(/^- `([^`]+)`(?:\s+—\s+(.+?))?\s*\(~(\d+)\s+tok\)$/); - if (em) { - sections.get(currentSection).push({ - file: em[1], - description: em[2] || "", - tokens: parseInt(em[3], 10), - }); - } - } - return sections; -} -export function serializeAnatomy(sections, metadata) { - const lines = [ - "# anatomy.md", - "", - `> Auto-maintained by OpenWolf. Last scanned: ${metadata.lastScanned}`, - `> Files: ${metadata.fileCount} tracked | Anatomy hits: ${metadata.hits} | Misses: ${metadata.misses}`, - "", - ]; - const keys = [...sections.keys()].sort(); - for (const key of keys) { - lines.push(`## ${key}`); - lines.push(""); - const entries = sections.get(key).sort((a, b) => a.file.localeCompare(b.file)); - for (const e of entries) { - const desc = e.description ? ` — ${e.description}` : ""; - lines.push(`- \`${e.file}\`${desc} (~${e.tokens} tok)`); - } - lines.push(""); - } - return lines.join("\n"); -} -export function extractDescription(filePath) { - const MAX_DESC = 150; - const basename = path.basename(filePath); - const ext = path.extname(basename).toLowerCase(); - const known = { - "package.json": "Node.js package manifest", - "tsconfig.json": "TypeScript configuration", - ".gitignore": "Git ignore rules", - "README.md": "Project documentation", - "composer.json": "PHP package manifest", - "requirements.txt": "Python dependencies", - "schema.sql": "Database schema", - "Dockerfile": "Docker container definition", - "docker-compose.yml": "Docker Compose services", - "Cargo.toml": "Rust package manifest", - "go.mod": "Go module definition", - "Gemfile": "Ruby dependencies", - "pubspec.yaml": "Dart/Flutter package manifest", - }; - if (known[basename]) - return known[basename]; - let content; - try { - const fd = fs.openSync(filePath, "r"); - const buf = Buffer.alloc(12288); // 12KB - const n = fs.readSync(fd, buf, 0, 12288, 0); - fs.closeSync(fd); - content = buf.subarray(0, n).toString("utf-8"); - } - catch { - return ""; - } - if (!content.trim()) - return ""; - const cap = (s) => s.length <= MAX_DESC ? s : s.slice(0, MAX_DESC - 3) + "..."; - // Markdown heading - if (ext === ".md" || ext === ".mdx") { - const m = content.match(/^#{1,2}\s+(.+)$/m); - if (m) - return cap(m[1].trim()); - } - // HTML title - if (ext === ".html" || ext === ".htm") { - const m = content.match(/]*>([^<]+)<\/title>/i); - if (m) - return cap(m[1].trim()); - } - // JSDoc / PHPDoc / Javadoc — first meaningful line - const jm = content.match(/\/\*\*\s*\n?\s*\*?\s*(.+)/); - if (jm) { - const l = jm[1].replace(/\*\/$/, "").trim(); - if (l && !l.startsWith("@") && l.length > 5) - return cap(l); - } - // Python docstring - if (ext === ".py") { - const dm = content.match(/^(?:#[^\n]*\n)*\s*(?:"""(.+?)"""|'''(.+?)''')/s); - if (dm) { - const first = (dm[1] || dm[2]).split("\n")[0].trim(); - if (first && first.length > 3) - return cap(first); - } - } - // Rust doc comments - if (ext === ".rs") { - const lines = content.split("\n"); - for (const line of lines.slice(0, 20)) { - const m = line.match(/^\s*(?:\/\/\/|\/\/!)\s*(.+)/); - if (m && m[1].length > 5) - return cap(m[1].trim()); - } - } - // Go package comment - if (ext === ".go") { - const m = content.match(/\/\/\s*Package\s+\w+\s+(.*)/); - if (m) - return cap(m[1].trim()); - } - // C# XML doc - if (ext === ".cs") { - const m = content.match(/\s*([\s\S]*?)\s*<\/summary>/); - if (m) { - const text = m[1].replace(/\/\/\/\s*/g, "").replace(/\s+/g, " ").trim(); - if (text.length > 5) - return cap(text); - } - } - // Elixir @moduledoc - if (ext === ".ex" || ext === ".exs") { - const m = content.match(/@moduledoc\s+"""\s*\n\s*(.*)/); - if (m) - return cap(m[1].trim()); - } - // Header comment (skip generic ones) - const hdrLines = content.split("\n"); - for (const line of hdrLines.slice(0, 15)) { - const t = line.trim(); - if (!t || t === " 5 && !lower.startsWith("copyright") && !lower.startsWith("license") && !lower.startsWith("@") && !lower.startsWith("strict") && !lower.startsWith("generated") && !lower.startsWith("eslint-") && !lower.startsWith("nolint")) { - return cap(text); - } - } - if (!t.startsWith("//") && !t.startsWith("#") && !t.startsWith("/*") && !t.startsWith("*") && !t.startsWith("--")) - break; - } - // ─── PHP / Laravel ─────────────────────────────────────── - if (ext === ".php") { - if (basename.endsWith(".blade.php")) { - const ext2 = content.match(/@extends\(\s*['"]([^'"]+)['"]\s*\)/); - const sections = (content.match(/@section\(\s*['"](\w+)['"]/g) || []).map(s => s.match(/['"](\w+)['"]/)?.[1]).filter(Boolean); - const parts = []; - if (ext2) - parts.push(`extends ${ext2[1]}`); - if (sections.length) - parts.push(`sections: ${sections.join(", ")}`); - return cap(parts.length ? `Blade: ${parts.join(", ")}` : "Blade template"); - } - const classM = content.match(/class\s+(\w+)(?:\s+extends\s+(\w+))?/); - const className = classM?.[1] || ""; - const parent = classM?.[2] || ""; - const pubMethods = (content.match(/public\s+function\s+(\w+)/g) || []) - .map(m => m.match(/public\s+function\s+(\w+)/)?.[1]) - .filter(n => n && n !== "__construct" && n !== "middleware"); - if (basename.endsWith("Controller.php") || parent === "Controller") { - if (pubMethods.length > 0) { - const display = pubMethods.slice(0, 5).join(", "); - return cap(pubMethods.length > 5 ? `${display} + ${pubMethods.length - 5} more` : display); - } - } - if (parent === "Model" || parent === "Authenticatable") { - const parts = []; - const tbl = content.match(/\$table\s*=\s*['"]([^'"]+)['"]/); - if (tbl) - parts.push(`table: ${tbl[1]}`); - const fill = content.match(/\$fillable\s*=\s*\[([^\]]*)\]/s); - if (fill) { - const c = (fill[1].match(/['"]/g) || []).length / 2; - parts.push(`${Math.floor(c)} fields`); - } - const rels = (content.match(/\$this->(hasMany|hasOne|belongsTo|belongsToMany|morphMany|morphTo)\(/g) || []).length; - if (rels) - parts.push(`${rels} rels`); - return cap(parts.length ? `Model — ${parts.join(", ")}` : `Model: ${className}`); - } - if (basename.match(/^\d{4}_\d{2}_\d{2}/)) { - const create = content.match(/Schema::create\(\s*['"]([^'"]+)['"]/); - if (create) - return `Migration: create ${create[1]} table`; - const alter = content.match(/Schema::table\(\s*['"]([^'"]+)['"]/); - if (alter) - return `Migration: alter ${alter[1]} table`; - return "Database migration"; - } - if (className && pubMethods.length > 0) { - const display = pubMethods.slice(0, 4).join(", "); - return cap(pubMethods.length > 4 ? `${className}: ${display} + ${pubMethods.length - 4} more` : `${className}: ${display}`); - } - } - // ─── TS/JS/React/Next.js ───────────────────────────────── - if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx" || ext === ".mjs" || ext === ".cjs") { - // React component - if (ext === ".tsx" || ext === ".jsx") { - const comp = content.match(/(?:export\s+(?:default\s+)?)?(?:function|const)\s+(\w+)/); - const parts = []; - if (comp) - parts.push(comp[1]); - const renders = []; - if (/<(?:form|Form)/i.test(content)) - renders.push("form"); - if (/<(?:table|Table|DataTable)/i.test(content)) - renders.push("table"); - if (/<(?:dialog|Dialog|Modal|Drawer)/i.test(content)) - renders.push("modal"); - if (renders.length) - parts.push(`renders ${renders.join(", ")}`); - if (parts.length) - return cap(parts.join(" — ")); - } - // Next.js conventions - if (basename === "page.tsx" || basename === "page.js") - return "Next.js page component"; - if (basename === "layout.tsx" || basename === "layout.js") - return "Next.js layout"; - if (basename === "route.ts" || basename === "route.js") { - const methods = [...new Set((content.match(/export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)/g) || []) - .map(m => m.match(/(GET|POST|PUT|PATCH|DELETE)/)?.[1]))].filter(Boolean); - return methods.length ? `Next.js API route: ${methods.join(", ")}` : "Next.js API route"; - } - // Express/Fastify routes - const routeHits = content.match(/\.(get|post|put|patch|delete)\s*\(\s*['"`]/g); - if (routeHits && routeHits.length > 0) { - const methods = [...new Set(routeHits.map(r => r.match(/\.(get|post|put|patch|delete)/)?.[1]?.toUpperCase()))]; - return cap(`API routes: ${methods.join(", ")} (${routeHits.length} endpoints)`); - } - // tRPC router - if (content.includes("createTRPCRouter") || content.includes("publicProcedure")) { - const procs = (content.match(/\.(query|mutation|subscription)\s*\(/g) || []).length; - return procs ? `tRPC router: ${procs} procedures` : "tRPC router"; - } - // Zod schemas - if (content.includes("z.object") || content.includes("z.string")) { - const schemas = (content.match(/(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*z\./g) || []) - .map(s => s.match(/(?:const|let)\s+(\w+)/)?.[1]).filter(Boolean); - if (schemas.length) - return cap(`Zod schemas: ${schemas.slice(0, 4).join(", ")}${schemas.length > 4 ? ` + ${schemas.length - 4} more` : ""}`); - } - // Exports summary - const exports = (content.match(/export\s+(?:async\s+)?(?:function|class|const|interface|type|enum)\s+(\w+)/g) || []) - .map(e => e.match(/(\w+)$/)?.[1]).filter(Boolean); - if (exports.length > 0 && exports.length <= 5) - return `Exports ${exports.join(", ")}`; - if (exports.length > 5) - return cap(`Exports ${exports.slice(0, 4).join(", ")} + ${exports.length - 4} more`); - } - // ─── Python / Django / FastAPI / Flask ──────────────────── - if (ext === ".py") { - // Django model - if (content.includes("models.Model")) { - const cls = content.match(/class\s+(\w+)\(.*models\.Model\)/); - const fields = (content.match(/^\s+\w+\s*=\s*models\.\w+/gm) || []).length; - return cap(`Model: ${cls?.[1] || "unknown"}, ${fields} fields`); - } - // FastAPI/Flask routes - if (content.includes("@router.") || content.includes("@app.")) { - const routes = (content.match(/@(?:router|app)\.(get|post|put|patch|delete)\s*\(/g) || []); - return cap(routes.length ? `API: ${routes.length} endpoints` : "API router"); - } - // Pydantic - if (content.includes("BaseModel") && content.includes("Field(")) { - const cls = content.match(/class\s+(\w+)\(.*BaseModel\)/); - return cls ? `Pydantic: ${cls[1]}` : "Pydantic model"; - } - // Celery - if (content.includes("@shared_task") || content.includes("@app.task")) { - const tasks = (content.match(/def\s+(\w+)/g) || []).map(m => m.match(/def\s+(\w+)/)?.[1]).filter(n => n && !n.startsWith("_")); - return cap(tasks.length ? `Celery tasks: ${tasks.join(", ")}` : "Celery task"); - } - // Generic - const pyClass = content.match(/class\s+(\w+)/); - const funcs = (content.match(/def\s+(\w+)/g) || []).map(f => f.match(/def\s+(\w+)/)?.[1]).filter(n => n && !n.startsWith("_")); - if (pyClass && funcs.length > 0) - return cap(funcs.length > 4 ? `${pyClass[1]}: ${funcs.slice(0, 4).join(", ")} + ${funcs.length - 4} more` : `${pyClass[1]}: ${funcs.join(", ")}`); - if (funcs.length > 0) - return cap(funcs.slice(0, 4).join(", ")); - } - // ─── Go ────────────────────────────────────────────────── - if (ext === ".go") { - const handlers = (content.match(/func\s+(\w+)\s*\(\s*\w+\s+http\.ResponseWriter/g) || []) - .map(m => m.match(/func\s+(\w+)/)?.[1]).filter(Boolean); - if (handlers.length) - return cap(`HTTP handlers: ${handlers.slice(0, 5).join(", ")}`); - const iface = content.match(/type\s+(\w+)\s+interface\s*\{/); - if (iface) - return `Interface: ${iface[1]}`; - const structM = content.match(/type\s+(\w+)\s+struct\s*\{/); - if (structM) - return `Struct: ${structM[1]}`; - const funcs = (content.match(/^func\s+(\w+)/gm) || []).map(m => m.match(/func\s+(\w+)/)?.[1]).filter(n => n && n[0] === n[0].toUpperCase()); - if (funcs.length) - return cap(funcs.slice(0, 5).join(", ")); - } - // ─── Rust ──────────────────────────────────────────────── - if (ext === ".rs") { - const structM = content.match(/pub\s+struct\s+(\w+)/); - if (structM) { - const methods = (content.match(/pub\s+(?:async\s+)?fn\s+(\w+)/g) || []).map(m => m.match(/fn\s+(\w+)/)?.[1]).filter(Boolean); - return cap(methods.length ? `${structM[1]}: ${methods.slice(0, 4).join(", ")}` : `Struct: ${structM[1]}`); - } - const traitM = content.match(/pub\s+trait\s+(\w+)/); - if (traitM) - return `Trait: ${traitM[1]}`; - const enumM = content.match(/pub\s+enum\s+(\w+)/); - if (enumM) - return `Enum: ${enumM[1]}`; - const fns = (content.match(/pub\s+(?:async\s+)?fn\s+(\w+)/g) || []).map(m => m.match(/fn\s+(\w+)/)?.[1]).filter(Boolean); - if (fns.length) - return cap(fns.slice(0, 5).join(", ")); - } - // ─── Java / Spring ─────────────────────────────────────── - if (ext === ".java") { - const cls = content.match(/(?:public\s+)?class\s+(\w+)/); - const className = cls?.[1] || basename.replace(".java", ""); - const annotations = (content.match(/@(RestController|Controller|Service|Repository|Component|Entity|Configuration)/g) || []).map(a => a.slice(1)); - const mappings = (content.match(/@(?:Get|Post|Put|Patch|Delete|Request)Mapping/g) || []).length; - if (mappings) - return cap(`${annotations[0] || "Spring"}: ${className} (${mappings} endpoints)`); - if (annotations.length) - return `${annotations[0]}: ${className}`; - if (content.includes("@Entity")) - return `Entity: ${className}`; - const methods = (content.match(/public\s+(?:static\s+)?(?:\w+(?:<[\w,\s]+>)?)\s+(\w+)\s*\(/g) || []) - .map(m => m.match(/(\w+)\s*\(/)?.[1]).filter(n => n && n !== className); - if (methods.length) - return cap(`${className}: ${methods.slice(0, 4).join(", ")}`); - return className ? `Class: ${className}` : ""; - } - // ─── Kotlin ────────────────────────────────────────────── - if (ext === ".kt" || ext === ".kts") { - const cls = content.match(/(?:data\s+)?class\s+(\w+)/); - if (content.match(/data\s+class/)) - return `Data class: ${cls?.[1] || basename.replace(/\.kts?$/, "")}`; - if (content.includes("routing {")) - return "Ktor routing"; - const fns = (content.match(/fun\s+(\w+)/g) || []).map(m => m.match(/fun\s+(\w+)/)?.[1]).filter(Boolean); - if (cls && fns.length) - return cap(`${cls[1]}: ${fns.slice(0, 4).join(", ")}`); - if (fns.length) - return cap(fns.slice(0, 5).join(", ")); - } - // ─── C# / .NET ─────────────────────────────────────────── - if (ext === ".cs") { - const cls = content.match(/(?:public\s+)?(?:partial\s+)?class\s+(\w+)(?:\s*:\s*(\w+))?/); - const className = cls?.[1] || basename.replace(".cs", ""); - const parent = cls?.[2] || ""; - if (parent === "Controller" || parent === "ControllerBase" || content.includes("[ApiController]")) { - const actions = (content.match(/\[Http(Get|Post|Put|Patch|Delete)\]/g) || []).map(a => a.match(/Http(\w+)/)?.[1]).filter(Boolean); - return cap(actions.length ? `API Controller: ${className} (${[...new Set(actions)].join(", ")})` : `Controller: ${className}`); - } - if (parent === "DbContext" || content.includes("DbSet<")) { - const sets = (content.match(/DbSet<(\w+)>/g) || []).map(s => s.match(/<(\w+)>/)?.[1]).filter(Boolean); - return cap(sets.length ? `DbContext: ${sets.join(", ")}` : `DbContext: ${className}`); - } - return className ? `Class: ${className}` : ""; - } - // ─── Ruby / Rails ──────────────────────────────────────── - if (ext === ".rb") { - const cls = content.match(/class\s+(\w+)(?:\s*<\s*(\w+(?:::\w+)?))?/); - const className = cls?.[1] || ""; - const parent = cls?.[2] || ""; - if (parent?.includes("Controller")) { - const actions = (content.match(/def\s+(index|show|new|create|edit|update|destroy|\w+)/g) || []) - .map(m => m.match(/def\s+(\w+)/)?.[1]).filter(n => n && !n.startsWith("_")); - return cap(actions.length ? `Controller: ${actions.join(", ")}` : `Controller: ${className}`); - } - if (parent === "ApplicationRecord" || parent === "ActiveRecord::Base") - return `Model: ${className}`; - if (basename.match(/^\d{14}_/)) { - const create = content.match(/create_table\s+:(\w+)/); - return create ? `Migration: create ${create[1]}` : "Database migration"; - } - const methods = (content.match(/def\s+(\w+)/g) || []).map(m => m.match(/def\s+(\w+)/)?.[1]).filter(n => n && !n.startsWith("_")); - if (cls && methods.length) - return cap(`${className}: ${methods.slice(0, 4).join(", ")}`); - } - // ─── Swift ─────────────────────────────────────────────── - if (ext === ".swift") { - if (content.includes(": View") || content.includes("some View")) { - const name = content.match(/struct\s+(\w+)\s*:\s*View/); - return name ? `SwiftUI view: ${name[1]}` : "SwiftUI view"; - } - const proto = content.match(/protocol\s+(\w+)/); - if (proto) - return `Protocol: ${proto[1]}`; - const struct = content.match(/(?:public\s+)?struct\s+(\w+)/); - const cls = content.match(/(?:public\s+)?class\s+(\w+)/); - const name = struct?.[1] || cls?.[1] || ""; - if (name) - return `${struct ? "Struct" : "Class"}: ${name}`; - } - // ─── Dart / Flutter ────────────────────────────────────── - if (ext === ".dart") { - if (content.includes("StatefulWidget") || content.includes("StatelessWidget")) { - const name = content.match(/class\s+(\w+)\s+extends\s+(?:Stateful|Stateless)Widget/); - return name ? `${content.includes("StatefulWidget") ? "Stateful" : "Stateless"} widget: ${name[1]}` : "Flutter widget"; - } - const cls = content.match(/class\s+(\w+)/); - if (cls) - return `Class: ${cls[1]}`; - } - // ─── Vue / Svelte / Astro ──────────────────────────────── - if (ext === ".vue") { - const name = content.match(/name:\s*['"]([^'"]+)['"]/); - const setup = content.includes("