Stop tracking .wolf/ — local OpenWolf state only
Replace the three partial .wolf patterns with a single .wolf/ rule and remove the directory from the index. Working-tree files stay in place. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+2
-3
@@ -8,9 +8,8 @@ __pycache__/
|
|||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
|
|
||||||
# OpenWolf runtime
|
# OpenWolf (all of it — local-only state)
|
||||||
.wolf/daemon.log
|
.wolf/
|
||||||
.wolf/designqc-captures/
|
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -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 <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`.
|
|
||||||
@@ -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)
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"bugs": []
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
<!-- How the user likes things done. Code style, tools, patterns, communication. -->
|
|
||||||
|
|
||||||
## 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 `<details>` block per page from `pdftotext -layout`, split on form-feed (`\f`).
|
|
||||||
- **`pdftocairo` filename quirk:** without `-singlefile` it appends `-<page>.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
|
|
||||||
|
|
||||||
<!-- Mistakes made and corrected. Each entry prevents the same mistake recurring. -->
|
|
||||||
<!-- Format: [YYYY-MM-DD] Description of what went wrong and what to do instead. -->
|
|
||||||
|
|
||||||
## Decision Log
|
|
||||||
|
|
||||||
<!-- Significant technical decisions with rationale. Why X was chosen over Y. -->
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"last_heartbeat": "2026-06-03T13:15:40.431Z",
|
|
||||||
"engine_status": "running",
|
|
||||||
"execution_log": [],
|
|
||||||
"dead_letter_queue": [],
|
|
||||||
"upcoming": []
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"captured_at": null,
|
|
||||||
"captures": [],
|
|
||||||
"total_size_kb": 0,
|
|
||||||
"estimated_tokens": 0
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "module"
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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[^>]*>([^<]+)<\/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(/<summary>\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 === "<?php" || t.startsWith("#!") || t.startsWith("namespace") || t.startsWith("use ") || t.startsWith("import ") || t.startsWith("from ") || t.startsWith("require") || t.startsWith("module "))
|
|
||||||
continue;
|
|
||||||
const cm = t.match(/^(?:\/\/|#|--)\s*(.+)/);
|
|
||||||
if (cm) {
|
|
||||||
const text = cm[1].trim();
|
|
||||||
const lower = text.toLowerCase();
|
|
||||||
if (text.length > 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("<script setup");
|
|
||||||
const parts = [];
|
|
||||||
if (name)
|
|
||||||
parts.push(name[1]);
|
|
||||||
if (setup)
|
|
||||||
parts.push("setup");
|
|
||||||
return cap(parts.length ? `Vue: ${parts.join(", ")}` : "Vue component");
|
|
||||||
}
|
|
||||||
if (ext === ".svelte")
|
|
||||||
return `Svelte: ${basename.replace(".svelte", "")}`;
|
|
||||||
if (ext === ".astro")
|
|
||||||
return `Astro: ${basename.replace(".astro", "")}`;
|
|
||||||
// ─── CSS / SCSS / Less ───────────────────────────────────
|
|
||||||
if (ext === ".css" || ext === ".scss" || ext === ".less") {
|
|
||||||
const rules = (content.match(/^[.#@][^\n{]+/gm) || []).length;
|
|
||||||
const vars = (content.match(/--[\w-]+\s*:/g) || []).length;
|
|
||||||
const parts = [];
|
|
||||||
if (rules)
|
|
||||||
parts.push(`${rules} rules`);
|
|
||||||
if (vars)
|
|
||||||
parts.push(`${vars} vars`);
|
|
||||||
return cap(parts.length ? `Styles: ${parts.join(", ")}` : "Stylesheet");
|
|
||||||
}
|
|
||||||
// ─── SQL ─────────────────────────────────────────────────
|
|
||||||
if (ext === ".sql") {
|
|
||||||
const creates = (content.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"']?(\w+)/gi) || [])
|
|
||||||
.map(m => m.match(/(?:TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?)([`"']?\w+)/i)?.[1]?.replace(/[`"']/g, "")).filter(Boolean);
|
|
||||||
if (creates.length)
|
|
||||||
return cap(`SQL: tables: ${creates.slice(0, 4).join(", ")}`);
|
|
||||||
}
|
|
||||||
// ─── Proto / GraphQL ─────────────────────────────────────
|
|
||||||
if (ext === ".proto") {
|
|
||||||
const msgs = (content.match(/message\s+(\w+)/g) || []).map(m => m.match(/message\s+(\w+)/)?.[1]).filter(Boolean);
|
|
||||||
const services = (content.match(/service\s+(\w+)/g) || []).map(m => m.match(/service\s+(\w+)/)?.[1]).filter(Boolean);
|
|
||||||
const parts = [];
|
|
||||||
if (msgs.length)
|
|
||||||
parts.push(`messages: ${msgs.slice(0, 3).join(", ")}`);
|
|
||||||
if (services.length)
|
|
||||||
parts.push(`services: ${services.join(", ")}`);
|
|
||||||
return cap(parts.length ? `Proto: ${parts.join(", ")}` : "");
|
|
||||||
}
|
|
||||||
if (ext === ".graphql" || ext === ".gql") {
|
|
||||||
const types = (content.match(/type\s+(\w+)/g) || []).map(m => m.match(/type\s+(\w+)/)?.[1]).filter(Boolean);
|
|
||||||
return cap(types.length ? `GraphQL: types: ${types.slice(0, 4).join(", ")}` : "GraphQL schema");
|
|
||||||
}
|
|
||||||
// ─── YAML ────────────────────────────────────────────────
|
|
||||||
if (ext === ".yaml" || ext === ".yml") {
|
|
||||||
if (content.includes("runs-on:")) {
|
|
||||||
const name = content.match(/^name:\s*(.+)$/m);
|
|
||||||
return cap(name ? `CI: ${name[1].trim()}` : "GitHub Actions workflow");
|
|
||||||
}
|
|
||||||
if (content.includes("apiVersion:") && content.includes("kind:")) {
|
|
||||||
const kind = content.match(/kind:\s*(\w+)/);
|
|
||||||
return cap(kind ? `K8s ${kind[1]}` : "Kubernetes manifest");
|
|
||||||
}
|
|
||||||
if (content.includes("services:") && (basename.includes("docker") || basename.includes("compose"))) {
|
|
||||||
const services = (content.match(/^\s{2}\w+:/gm) || []).length;
|
|
||||||
return `Docker Compose: ${services} services`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ─── TOML ────────────────────────────────────────────────
|
|
||||||
if (ext === ".toml") {
|
|
||||||
const desc = content.match(/^description\s*=\s*"([^"]+)"/m);
|
|
||||||
if (desc)
|
|
||||||
return cap(desc[1]);
|
|
||||||
}
|
|
||||||
// ─── Elixir ──────────────────────────────────────────────
|
|
||||||
if (ext === ".ex" || ext === ".exs") {
|
|
||||||
const mod = content.match(/defmodule\s+([\w.]+)/);
|
|
||||||
if (content.includes("Phoenix.LiveView"))
|
|
||||||
return cap(mod ? `LiveView: ${mod[1]}` : "Phoenix LiveView");
|
|
||||||
if (content.includes("Controller"))
|
|
||||||
return cap(mod ? `Phoenix controller: ${mod[1]}` : "Phoenix controller");
|
|
||||||
const fns = (content.match(/def\s+(\w+)/g) || []).map(m => m.match(/def\s+(\w+)/)?.[1]).filter(Boolean);
|
|
||||||
if (mod && fns.length)
|
|
||||||
return cap(`${mod[1]}: ${fns.slice(0, 4).join(", ")}`);
|
|
||||||
if (mod)
|
|
||||||
return mod[1];
|
|
||||||
}
|
|
||||||
// ─── Lua ─────────────────────────────────────────────────
|
|
||||||
if (ext === ".lua") {
|
|
||||||
const fns = (content.match(/function\s+(?:\w+[.:])?(\w+)/g) || []).map(m => m.match(/(\w+)\s*$/)?.[1]).filter(Boolean);
|
|
||||||
if (fns.length)
|
|
||||||
return cap(fns.slice(0, 5).join(", "));
|
|
||||||
}
|
|
||||||
// ─── Zig ─────────────────────────────────────────────────
|
|
||||||
if (ext === ".zig") {
|
|
||||||
const fns = (content.match(/pub\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(", "));
|
|
||||||
}
|
|
||||||
// Last resort
|
|
||||||
const declM = content.match(/(?:function|class|const|interface|type|enum)\s+(\w+)/);
|
|
||||||
if (declM) {
|
|
||||||
const name = declM[1];
|
|
||||||
const methods = (content.match(/(?:public\s+)?(?:async\s+)?(?:function\s+|(?:get|set)\s+)(\w+)\s*\(/g) || [])
|
|
||||||
.map(m => m.match(/(\w+)\s*\(/)?.[1]).filter(n => n && n !== name && n !== "__construct" && n !== "constructor");
|
|
||||||
if (methods.length > 0 && methods.length <= 5)
|
|
||||||
return cap(`${name}: ${methods.join(", ")}`);
|
|
||||||
if (methods.length > 5)
|
|
||||||
return cap(`${name}: ${methods.slice(0, 3).join(", ")} + ${methods.length - 3} more`);
|
|
||||||
return `Declares ${name}`;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
export function estimateTokens(text, type = "mixed") {
|
|
||||||
const ratio = type === "code" ? 3.5 : type === "prose" ? 4.0 : 3.75;
|
|
||||||
return Math.ceil(text.length / ratio);
|
|
||||||
}
|
|
||||||
export function timestamp() {
|
|
||||||
return new Date().toISOString();
|
|
||||||
}
|
|
||||||
export function timeShort() {
|
|
||||||
const d = new Date();
|
|
||||||
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
||||||
}
|
|
||||||
export function readStdin() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const chunks = [];
|
|
||||||
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
||||||
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
||||||
// If no stdin data after 4s, resolve with whatever we have so far.
|
|
||||||
// On Windows, stdin delivery from Claude Code hooks can be slow.
|
|
||||||
setTimeout(() => resolve(chunks.length ? Buffer.concat(chunks).toString("utf-8") : "{}"), 4000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
export function normalizePath(p) {
|
|
||||||
return p.replace(/\\/g, "/");
|
|
||||||
}
|
|
||||||
//# sourceMappingURL=shared.js.map
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
import * as fs from "node:fs";
|
|
||||||
import * as path from "node:path";
|
|
||||||
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, appendMarkdown, timeShort } from "./shared.js";
|
|
||||||
async function main() {
|
|
||||||
ensureWolfDir();
|
|
||||||
const wolfDir = getWolfDir();
|
|
||||||
const hooksDir = path.join(wolfDir, "hooks");
|
|
||||||
const sessionFile = path.join(hooksDir, "_session.json");
|
|
||||||
const session = readJSON(sessionFile, {
|
|
||||||
session_id: "",
|
|
||||||
started: "",
|
|
||||||
files_read: {},
|
|
||||||
files_written: [],
|
|
||||||
edit_counts: {},
|
|
||||||
anatomy_hits: 0,
|
|
||||||
anatomy_misses: 0,
|
|
||||||
repeated_reads_warned: 0,
|
|
||||||
cerebrum_warnings: 0,
|
|
||||||
stop_count: 0,
|
|
||||||
});
|
|
||||||
session.stop_count++;
|
|
||||||
// Only write to ledger if there's been activity
|
|
||||||
const readCount = Object.keys(session.files_read).length;
|
|
||||||
const writeCount = session.files_written.length;
|
|
||||||
if (readCount === 0 && writeCount === 0) {
|
|
||||||
writeJSON(sessionFile, session);
|
|
||||||
process.exit(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Check for files edited many times without a buglog entry
|
|
||||||
checkForMissingBugLogs(wolfDir, session);
|
|
||||||
// Check if cerebrum was updated this session (it should be if there were edits)
|
|
||||||
checkCerebrumFreshness(wolfDir, session);
|
|
||||||
// Build session entry for ledger
|
|
||||||
const reads = Object.entries(session.files_read).map(([file, data]) => ({
|
|
||||||
file,
|
|
||||||
tokens_estimated: data.tokens,
|
|
||||||
was_repeated: data.count > 1,
|
|
||||||
anatomy_had_description: false, // simplified
|
|
||||||
}));
|
|
||||||
const writes = session.files_written.map((w) => ({
|
|
||||||
file: w.file,
|
|
||||||
tokens_estimated: w.tokens,
|
|
||||||
action: w.action,
|
|
||||||
}));
|
|
||||||
const inputTokens = reads.reduce((sum, r) => sum + r.tokens_estimated, 0);
|
|
||||||
const outputTokens = writes.reduce((sum, w) => sum + w.tokens_estimated, 0);
|
|
||||||
const sessionEntry = {
|
|
||||||
id: session.session_id,
|
|
||||||
started: session.started,
|
|
||||||
ended: new Date().toISOString(),
|
|
||||||
reads,
|
|
||||||
writes,
|
|
||||||
totals: {
|
|
||||||
input_tokens_estimated: inputTokens,
|
|
||||||
output_tokens_estimated: outputTokens,
|
|
||||||
reads_count: readCount,
|
|
||||||
writes_count: writeCount,
|
|
||||||
repeated_reads_blocked: session.repeated_reads_warned,
|
|
||||||
anatomy_lookups: session.anatomy_hits,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// Update token-ledger.json
|
|
||||||
const ledgerPath = path.join(wolfDir, "token-ledger.json");
|
|
||||||
const ledger = readJSON(ledgerPath, {
|
|
||||||
version: 1,
|
|
||||||
created_at: "",
|
|
||||||
lifetime: {
|
|
||||||
total_tokens_estimated: 0,
|
|
||||||
total_reads: 0,
|
|
||||||
total_writes: 0,
|
|
||||||
total_sessions: 0,
|
|
||||||
anatomy_hits: 0,
|
|
||||||
anatomy_misses: 0,
|
|
||||||
repeated_reads_blocked: 0,
|
|
||||||
estimated_savings_vs_bare_cli: 0,
|
|
||||||
},
|
|
||||||
sessions: [],
|
|
||||||
daemon_usage: [],
|
|
||||||
waste_flags: [],
|
|
||||||
optimization_report: { last_generated: null, patterns: [] },
|
|
||||||
});
|
|
||||||
ledger.sessions.push(sessionEntry);
|
|
||||||
ledger.lifetime.total_reads += readCount;
|
|
||||||
ledger.lifetime.total_writes += writeCount;
|
|
||||||
ledger.lifetime.total_tokens_estimated += inputTokens + outputTokens;
|
|
||||||
ledger.lifetime.anatomy_hits += session.anatomy_hits;
|
|
||||||
ledger.lifetime.anatomy_misses += session.anatomy_misses;
|
|
||||||
ledger.lifetime.repeated_reads_blocked += session.repeated_reads_warned;
|
|
||||||
// Estimate savings: anatomy hits save ~200 tokens each, repeated reads blocked save their token count
|
|
||||||
const savedFromAnatomy = session.anatomy_hits * 200;
|
|
||||||
const savedFromRepeats = Object.values(session.files_read)
|
|
||||||
.filter((r) => r.count > 1)
|
|
||||||
.reduce((sum, r) => sum + r.tokens * (r.count - 1), 0);
|
|
||||||
ledger.lifetime.estimated_savings_vs_bare_cli += savedFromAnatomy + savedFromRepeats;
|
|
||||||
writeJSON(ledgerPath, ledger);
|
|
||||||
// Write a session summary line to memory.md if there was meaningful activity
|
|
||||||
if (writeCount > 0) {
|
|
||||||
try {
|
|
||||||
const uniqueFiles = new Set(session.files_written.map(w => path.basename(w.file)));
|
|
||||||
const fileList = [...uniqueFiles].slice(0, 5).join(", ");
|
|
||||||
const memoryPath = path.join(wolfDir, "memory.md");
|
|
||||||
appendMarkdown(memoryPath, `| ${timeShort()} | Session end: ${writeCount} writes across ${uniqueFiles.size} files (${fileList}) | ${readCount} reads | ~${inputTokens + outputTokens} tok |\n`);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
writeJSON(sessionFile, session);
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Check if files were edited multiple times but buglog.json wasn't updated.
|
|
||||||
* Emit a stderr reminder so Claude sees it in the next turn.
|
|
||||||
*/
|
|
||||||
function checkForMissingBugLogs(wolfDir, session) {
|
|
||||||
if (!session.edit_counts)
|
|
||||||
return;
|
|
||||||
const multiEditFiles = Object.entries(session.edit_counts)
|
|
||||||
.filter(([, count]) => count >= 3)
|
|
||||||
.map(([file]) => path.basename(file));
|
|
||||||
if (multiEditFiles.length === 0)
|
|
||||||
return;
|
|
||||||
// Check if buglog was written to this session
|
|
||||||
const buglogWritten = session.files_written.some(w => w.file.includes("buglog.json"));
|
|
||||||
if (!buglogWritten) {
|
|
||||||
process.stderr.write(`⚠️ OpenWolf: Files edited 3+ times this session (${multiEditFiles.join(", ")}) but buglog.json was not updated. If you fixed bugs, please log them.\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Check if cerebrum.md was updated recently. If it hasn't been updated in
|
|
||||||
* a while and there was significant activity, emit a gentle reminder.
|
|
||||||
*/
|
|
||||||
function checkCerebrumFreshness(wolfDir, session) {
|
|
||||||
const cerebrumPath = path.join(wolfDir, "cerebrum.md");
|
|
||||||
try {
|
|
||||||
const stat = fs.statSync(cerebrumPath);
|
|
||||||
const hoursSinceUpdate = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60);
|
|
||||||
// If cerebrum hasn't been updated in 24h+ and there were significant writes
|
|
||||||
if (hoursSinceUpdate > 24 && session.files_written.length >= 3) {
|
|
||||||
process.stderr.write(`💡 OpenWolf: cerebrum.md hasn't been updated in ${Math.floor(hoursSinceUpdate)}h. Did you learn any user preferences, conventions, or gotchas this session? Consider updating .wolf/cerebrum.md.\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// cerebrum.md doesn't exist, that's ok
|
|
||||||
}
|
|
||||||
}
|
|
||||||
main().catch(() => process.exit(0));
|
|
||||||
//# sourceMappingURL=stop.js.map
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# Identity
|
|
||||||
|
|
||||||
- **Name:** netgazet
|
|
||||||
- **Role:** AI development assistant for netgazet
|
|
||||||
- **Tone:** Direct, concise, technically precise
|
|
||||||
- **Constraints:**
|
|
||||||
- Never modify .env or secret files without explicit user confirmation
|
|
||||||
- Never delete files without explicit user confirmation
|
|
||||||
- Always explain why before making architectural changes
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# Memory
|
|
||||||
|
|
||||||
> Chronological action log. Hooks and AI append to this file automatically.
|
|
||||||
> Old sessions are consolidated by the daemon weekly.
|
|
||||||
|
|
||||||
## Session: 2026-06-03 15:15
|
|
||||||
|
|
||||||
| Time | Action | File(s) | Outcome | ~Tokens |
|
|
||||||
|------|--------|---------|---------|--------|
|
|
||||||
|
|
||||||
## Session: 2026-06-03 15:16
|
|
||||||
|
|
||||||
| Time | Action | File(s) | Outcome | ~Tokens |
|
|
||||||
|------|--------|---------|---------|--------|
|
|
||||||
| 15:16 | Created CLAUDE.md | — | ~587 |
|
|
||||||
| 15:16 | Session end: 1 writes across 1 files (CLAUDE.md) | 0 reads | ~628 tok |
|
|
||||||
| 15:17 | Session end: 1 writes across 1 files (CLAUDE.md) | 1 reads | ~1178 tok |
|
|
||||||
| 15:25 | Created ../../.claude/plans/in-the-example-folder-immutable-rossum.md | — | ~2254 |
|
|
||||||
| 15:29 | Created main.py | — | ~2847 |
|
|
||||||
| 15:30 | Implemented PDF→HTML converter (poppler-utils, stdlib only) | main.py | 32 pages rendered, output/ ≈ 20MB | ~3000 |
|
|
||||||
| 15:30 | Generated output/ site | output/index.html, output/styles.css, output/NETgazet-07_okt2024/*, output/NETgazet-08_apr2025/* | 2 issues × 16 pages | ~500 |
|
|
||||||
| 15:30 | Session end: 3 writes across 3 files (CLAUDE.md, in-the-example-folder-immutable-rossum.md, main.py) | 4 reads | ~6439 tok |
|
|
||||||
| 15:34 | Session end: 3 writes across 3 files (CLAUDE.md, in-the-example-folder-immutable-rossum.md, main.py) | 4 reads | ~6439 tok |
|
|
||||||
| 15:35 | Session end: 3 writes across 3 files (CLAUDE.md, in-the-example-folder-immutable-rossum.md, main.py) | 4 reads | ~6439 tok |
|
|
||||||
|
|
||||||
## Session: 2026-06-03 15:52
|
|
||||||
|
|
||||||
| Time | Action | File(s) | Outcome | ~Tokens |
|
|
||||||
|------|--------|---------|---------|--------|
|
|
||||||
|
|
||||||
## Session: 2026-06-03 15:55
|
|
||||||
|
|
||||||
| Time | Action | File(s) | Outcome | ~Tokens |
|
|
||||||
|------|--------|---------|---------|--------|
|
|
||||||
| 15:57 | Created README.md | — | ~970 |
|
|
||||||
| 15:57 | Created .gitignore | — | ~55 |
|
|
||||||
@@ -1,597 +0,0 @@
|
|||||||
# OpenWolf Reframe — UI Framework Knowledge Base
|
|
||||||
|
|
||||||
When the user asks to change, pick, migrate, or "reframe" their UI framework, use this file to guide the conversation and generate the right migration prompt.
|
|
||||||
|
|
||||||
## Decision Questions
|
|
||||||
|
|
||||||
Ask these in order. Stop early if the answer narrows to 1-2 frameworks.
|
|
||||||
|
|
||||||
1. **What framework/library does your project currently use?** (React, Vue, Svelte, plain HTML, etc.)
|
|
||||||
2. **What's your priority?** (a) Stunning animations, (b) Fast setup / ship quickly, (c) Full design control, (d) Accessibility-first, (e) Business/enterprise look
|
|
||||||
3. **Do you use Tailwind CSS already?** (Yes → most options work. No → Chakra UI or DaisyUI are easier without it)
|
|
||||||
4. **Light mode, dark mode, or both?**
|
|
||||||
5. **Is this a landing page, a dashboard/app, or both?**
|
|
||||||
|
|
||||||
## Quick Selection Guide
|
|
||||||
|
|
||||||
| Priority | Recommended |
|
|
||||||
|----------|-------------|
|
|
||||||
| Fastest setup | DaisyUI, Preline UI |
|
|
||||||
| Stunning animations | Aceternity UI, Magic UI |
|
|
||||||
| Maximum control | shadcn/ui, Headless UI |
|
|
||||||
| Most free components | Origin UI (400+) |
|
|
||||||
| Multi-framework (React + Vue + Svelte) | Park UI, DaisyUI, Flowbite |
|
|
||||||
| AI product aesthetic | Cult UI |
|
|
||||||
| Polished defaults + accessibility | HeroUI, Chakra UI |
|
|
||||||
| Business/enterprise | Flowbite, shadcn/ui |
|
|
||||||
|
|
||||||
## Comparison Matrix
|
|
||||||
|
|
||||||
| Framework | Styling | Animation | Setup | Best For | Cost |
|
|
||||||
|-----------|---------|-----------|-------|----------|------|
|
|
||||||
| shadcn/ui | Tailwind + Radix | Minimal | Medium | Full apps, dashboards | Free |
|
|
||||||
| Aceternity UI | Tailwind + Framer Motion | Heavy, cinematic | Easy (copy-paste) | Animated landing pages | Free / Pro |
|
|
||||||
| Magic UI | Tailwind + Motion | Purposeful, polished | Easy (shadcn CLI) | SaaS marketing | Free / Pro |
|
|
||||||
| DaisyUI | Tailwind plugin | CSS transitions | Very easy | Rapid prototyping | Free |
|
|
||||||
| HeroUI | Tailwind + React Aria | Smooth built-in | Easy | React apps, accessible | Free |
|
|
||||||
| Chakra UI | CSS-in-JS (Emotion) | Built-in transitions | Easy | Themed React apps | Free |
|
|
||||||
| Flowbite | Tailwind plugin | CSS transitions | Very easy | Multi-framework, business | Free / Pro |
|
|
||||||
| Preline UI | Tailwind plugin | CSS + minimal JS | Very easy | Speed-focused builds | Free / Pro |
|
|
||||||
| Park UI | Tailwind + Ark UI | Minimal | Medium | Multi-framework, design systems | Free |
|
|
||||||
| Origin UI | Tailwind + shadcn | Minimal | Easy (copy-paste) | Max component variety | Free |
|
|
||||||
| Headless UI | Custom Tailwind | Custom (Transition) | Medium-Hard | Full design control | Free |
|
|
||||||
| Cult UI | Tailwind + shadcn | Modern, subtle | Medium | AI apps, full-stack | Free |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Framework Prompts
|
|
||||||
|
|
||||||
After the user selects a framework, use the corresponding prompt below. **Adapt it to the user's actual project** — replace generic references with their real file structure, existing routes, and components from `.wolf/anatomy.md`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### shadcn/ui
|
|
||||||
|
|
||||||
**Stack:** React, Tailwind CSS, Radix UI
|
|
||||||
**Install:** `npx shadcn@latest init`
|
|
||||||
**Site:** ui.shadcn.com
|
|
||||||
|
|
||||||
```
|
|
||||||
Build a modern, minimalist landing page using Next.js 14+ (App Router), TypeScript, Tailwind CSS, and shadcn/ui components.
|
|
||||||
|
|
||||||
ARCHITECTURE:
|
|
||||||
- Use Next.js App Router with server components by default, client components only when interactivity is needed
|
|
||||||
- Organize: /app for routes, /components/ui for shadcn primitives, /components/sections for page sections, /lib for utilities
|
|
||||||
- Use CSS variables for theming via shadcn's built-in system
|
|
||||||
|
|
||||||
DESIGN PRINCIPLES:
|
|
||||||
- Minimalist with generous whitespace (py-24 to py-32 section spacing)
|
|
||||||
- Max-width container (max-w-6xl mx-auto) with responsive padding
|
|
||||||
- Typography hierarchy: text-5xl/6xl hero → text-3xl section titles → text-lg body
|
|
||||||
- Muted color palette with ONE bold accent color for CTAs
|
|
||||||
- Subtle micro-interactions on hover (scale, opacity transitions)
|
|
||||||
- Dark mode support via shadcn's theme provider
|
|
||||||
|
|
||||||
SECTIONS TO BUILD:
|
|
||||||
1. Navbar — sticky, transparent-to-solid on scroll, logo left, nav center, CTA button right
|
|
||||||
2. Hero — large headline (max 8 words), subtext (max 2 lines), primary CTA button + secondary ghost button, optional hero image or abstract illustration
|
|
||||||
3. Social proof — logo cloud of 5-6 partner/client logos in grayscale
|
|
||||||
4. Features — 3-column bento grid with icon, title, description per card using shadcn Card component
|
|
||||||
5. How it works — 3-step numbered process with connecting line
|
|
||||||
6. Testimonials — carousel or grid of 3 testimonial cards with avatar, quote, name, role
|
|
||||||
7. Pricing — 3-tier pricing table using shadcn Card, highlight the recommended plan with ring/border accent
|
|
||||||
8. FAQ — accordion using shadcn Accordion component
|
|
||||||
9. CTA — full-width section with headline, subtext, and primary action button
|
|
||||||
10. Footer — 4-column grid (brand, product links, company links, legal), copyright at bottom
|
|
||||||
|
|
||||||
COMPONENT USAGE:
|
|
||||||
- Button (primary, secondary, ghost, outline variants)
|
|
||||||
- Card, CardHeader, CardContent, CardFooter
|
|
||||||
- Accordion, AccordionItem, AccordionTrigger, AccordionContent
|
|
||||||
- Badge for labels/tags
|
|
||||||
- Separator for dividers
|
|
||||||
- NavigationMenu for navbar
|
|
||||||
|
|
||||||
CODE QUALITY:
|
|
||||||
- Fully responsive (mobile-first)
|
|
||||||
- Semantic HTML (section, nav, main, footer)
|
|
||||||
- Accessible (proper aria labels, keyboard navigation)
|
|
||||||
- Performant (lazy load images, optimize fonts via next/font)
|
|
||||||
- Clean component separation (each section is its own component)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Aceternity UI
|
|
||||||
|
|
||||||
**Stack:** React, Next.js, Tailwind CSS, Framer Motion
|
|
||||||
**Install:** Copy-paste or `npx shadcn@latest add [component]`
|
|
||||||
**Site:** ui.aceternity.com
|
|
||||||
|
|
||||||
```
|
|
||||||
Build a visually stunning, animation-rich landing page using Next.js 14+, TypeScript, Tailwind CSS, and Aceternity UI components with Framer Motion.
|
|
||||||
|
|
||||||
ARCHITECTURE:
|
|
||||||
- Next.js App Router with client components for animated sections
|
|
||||||
- Organize: /app, /components/aceternity (copied components), /components/sections, /lib
|
|
||||||
- Install Framer Motion for animation engine
|
|
||||||
|
|
||||||
DESIGN PRINCIPLES:
|
|
||||||
- Dark theme as primary (bg-black/bg-slate-950 backgrounds)
|
|
||||||
- Dramatic contrast with glowing accents (cyan, purple, or emerald)
|
|
||||||
- Layered depth via gradients, glows, and glassmorphism
|
|
||||||
- Cinematic scroll-triggered animations with staggered reveals
|
|
||||||
- Generous padding (py-32+), large typography (text-6xl/7xl hero)
|
|
||||||
- Noise/grain texture overlays for visual richness
|
|
||||||
|
|
||||||
SECTIONS TO BUILD WITH ACETERNITY COMPONENTS:
|
|
||||||
1. Navbar — floating navbar with backdrop blur, animated on scroll
|
|
||||||
2. Hero — use Aceternity's "Spotlight" or "Lamp" effect as background, massive headline with TextGenerateEffect or TypewriterEffect, gradient text on key words, two CTAs with HoverBorderGradient buttons
|
|
||||||
3. Logo cloud — use InfiniteMovingCards for auto-scrolling partner logos
|
|
||||||
4. Features — BentoGrid layout with animated cards, each card has BackgroundGradient or CardSpotlight hover effect
|
|
||||||
5. Product showcase — use Aceternity's 3DCard or ParallaxScroll for product screenshots/mockups
|
|
||||||
6. Testimonials — AnimatedTestimonials or InfiniteMovingCards with quote, avatar, name
|
|
||||||
7. How it works — use TracingBeam component for a connected step-by-step flow
|
|
||||||
8. Pricing — dark cards with GlowingStarsBackground, highlighted tier has animated border (MovingBorder)
|
|
||||||
9. CTA — full-width with BackgroundBeams or Meteors effect behind headline and button
|
|
||||||
10. Footer — clean 4-column layout with subtle separator
|
|
||||||
|
|
||||||
ANIMATION GUIDELINES:
|
|
||||||
- Page load: stagger hero elements (title → subtitle → buttons) with 0.15s delay each
|
|
||||||
- Scroll: use Framer Motion's whileInView for reveal animations (fade up + slight scale)
|
|
||||||
- Hover: cards lift (translateY -4px) with glow intensification
|
|
||||||
- Keep animations performant (use transform/opacity only, will-change where needed)
|
|
||||||
- Respect prefers-reduced-motion
|
|
||||||
|
|
||||||
CODE QUALITY:
|
|
||||||
- Mobile-first responsive design
|
|
||||||
- Lazy load heavy animation components
|
|
||||||
- Each section is a standalone component file
|
|
||||||
- Accessible despite heavy animation (aria labels, focus states)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Magic UI
|
|
||||||
|
|
||||||
**Stack:** React, TypeScript, Tailwind CSS, Motion (Framer Motion)
|
|
||||||
**Install:** `npx shadcn@latest add [component]` (shadcn CLI compatible)
|
|
||||||
**Site:** magicui.design
|
|
||||||
|
|
||||||
```
|
|
||||||
Build a polished, trend-forward SaaS landing page using Next.js 14+, TypeScript, Tailwind CSS, and Magic UI animated components.
|
|
||||||
|
|
||||||
ARCHITECTURE:
|
|
||||||
- Next.js App Router, server components where possible
|
|
||||||
- Magic UI components installed via shadcn CLI into /components/magicui
|
|
||||||
- Section components in /components/sections
|
|
||||||
- Shared config in /lib (cn utility, site config)
|
|
||||||
|
|
||||||
DESIGN PRINCIPLES:
|
|
||||||
- Clean and modern, inspired by Linear/Vercel aesthetic
|
|
||||||
- Light or dark mode with smooth toggle transition
|
|
||||||
- Restrained animation — purposeful motion that guides attention, not distracts
|
|
||||||
- High contrast text, muted backgrounds (slate-50 light / slate-950 dark)
|
|
||||||
- Monospace or geometric sans-serif for headings, clean sans for body
|
|
||||||
- ONE signature color (blue-500, violet-500, or emerald-500) for accents
|
|
||||||
|
|
||||||
SECTIONS TO BUILD WITH MAGIC UI COMPONENTS:
|
|
||||||
1. Navbar — clean horizontal nav, use MagicUI ShimmerButton for primary CTA
|
|
||||||
2. Hero — large headline with AnimatedGradientText on key phrase, NumberTicker for a live stat ("10,000+ users"), DotPattern or GridPattern as subtle background, two buttons (ShimmerButton primary + outline secondary)
|
|
||||||
3. Social proof — Marquee component for auto-scrolling logos horizontally
|
|
||||||
4. Features — use MagicCard components in a 3-column grid, each with icon + title + description, subtle hover glow effect
|
|
||||||
5. Bento showcase — BentoGrid layout showing product capabilities, mix of large and small tiles with different content types (text, mini-demos, stats)
|
|
||||||
6. Metrics — AnimatedList showing live activity feed OR NumberTicker showing key stats (users, uptime, speed)
|
|
||||||
7. Testimonials — Marquee of testimonial cards (vertical or horizontal scroll)
|
|
||||||
8. Pricing — 3 tiers with clean card design, NeonGradientCard for featured plan, BorderBeam on hover
|
|
||||||
9. CTA — centered layout with AnimatedShinyText headline, Particles or Meteors background effect
|
|
||||||
10. Footer — minimal 3-4 column grid, muted text, social icons
|
|
||||||
|
|
||||||
ANIMATION STRATEGY:
|
|
||||||
- Use BlurIn / FadeIn for section entrance animations
|
|
||||||
- NumberTicker for any statistics or metrics
|
|
||||||
- Marquee for any horizontally scrolling content
|
|
||||||
- Keep transitions under 600ms, use ease-out curves
|
|
||||||
- Mobile: reduce or disable particle/meteor effects for performance
|
|
||||||
|
|
||||||
CODE QUALITY:
|
|
||||||
- Fully responsive with mobile breakpoints
|
|
||||||
- Semantic HTML throughout
|
|
||||||
- next/font for optimized typography
|
|
||||||
- Image optimization via next/image
|
|
||||||
- Component-per-section architecture
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### DaisyUI
|
|
||||||
|
|
||||||
**Stack:** Tailwind CSS plugin (framework-agnostic)
|
|
||||||
**Install:** `npm i -D daisyui` + add to tailwind.config
|
|
||||||
**Site:** daisyui.com
|
|
||||||
|
|
||||||
```
|
|
||||||
Build a clean, professional landing page using Tailwind CSS and DaisyUI component classes.
|
|
||||||
|
|
||||||
ARCHITECTURE:
|
|
||||||
- Framework of choice with Tailwind CSS + DaisyUI plugin
|
|
||||||
- Use DaisyUI's semantic class system (btn, card, hero, navbar, etc.)
|
|
||||||
- Select a DaisyUI theme (or create custom) in tailwind.config.js
|
|
||||||
- Organize by sections in /components/sections
|
|
||||||
|
|
||||||
DESIGN PRINCIPLES:
|
|
||||||
- Pick ONE DaisyUI theme as base (e.g., "winter" for clean light, "night" for dark, "corporate" for professional)
|
|
||||||
- Customize theme colors in tailwind.config to match brand
|
|
||||||
- Minimalist layout with clean spacing (p-8 to p-16 sections)
|
|
||||||
- Let DaisyUI's built-in design system handle consistency
|
|
||||||
- Typography via DaisyUI's prose class for content sections
|
|
||||||
|
|
||||||
SECTIONS TO BUILD WITH DAISYUI CLASSES:
|
|
||||||
1. Navbar — "navbar" component with "dropdown" for mobile menu, "btn btn-primary" for CTA
|
|
||||||
2. Hero — "hero" component with "hero-content", large headline, subtext, "btn btn-primary" + "btn btn-ghost"
|
|
||||||
3. Social proof — logo row using "flex" with "opacity-50 grayscale" on images
|
|
||||||
4. Features — "card" components in a responsive grid, each with "card-body", uses "bg-base-200" for contrast
|
|
||||||
5. Stats — "stats" component with "stat" items showing key metrics
|
|
||||||
6. Steps — "steps" component (horizontal on desktop, vertical on mobile)
|
|
||||||
7. Testimonials — "chat" component styled as testimonials OR "card" grid with "avatar"
|
|
||||||
8. Pricing — "card" components with "badge badge-primary" for recommended plan
|
|
||||||
9. FAQ — "collapse" or "collapse-plus" components for expandable Q&A
|
|
||||||
10. Footer — "footer" component with 3-4 columns, "footer-title" for headings
|
|
||||||
|
|
||||||
CODE QUALITY:
|
|
||||||
- Fully responsive using Tailwind breakpoints
|
|
||||||
- Zero custom CSS needed (DaisyUI handles it)
|
|
||||||
- Accessible by default (DaisyUI follows ARIA patterns)
|
|
||||||
- Fast load times (CSS-only, no JS shipped)
|
|
||||||
- Works with any framework or plain HTML
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### HeroUI (formerly NextUI)
|
|
||||||
|
|
||||||
**Stack:** React, Tailwind CSS, React Aria
|
|
||||||
**Install:** `npm i @heroui/react`
|
|
||||||
**Site:** heroui.com
|
|
||||||
|
|
||||||
```
|
|
||||||
Build an elegant, accessible landing page using Next.js 14+, TypeScript, Tailwind CSS, and HeroUI components.
|
|
||||||
|
|
||||||
ARCHITECTURE:
|
|
||||||
- Next.js App Router with HeroUI Provider wrapping the app
|
|
||||||
- Import only needed components from @heroui/react (tree-shakeable)
|
|
||||||
- Organize: /app, /components/sections, /lib
|
|
||||||
- Use HeroUI's built-in theme system for consistent styling
|
|
||||||
|
|
||||||
DESIGN PRINCIPLES:
|
|
||||||
- Soft, modern aesthetic with rounded corners and smooth surfaces
|
|
||||||
- Light mode primary with dark mode support via HeroUI's theme toggle
|
|
||||||
- Subtle shadows and blur effects (HeroUI's built-in elevation system)
|
|
||||||
- Clean sans-serif typography, generous line heights
|
|
||||||
- Smooth transitions on all interactive elements (HeroUI provides these by default)
|
|
||||||
|
|
||||||
SECTIONS TO BUILD WITH HEROUI COMPONENTS:
|
|
||||||
1. Navbar — HeroUI Navbar with NavbarBrand, NavbarContent, NavbarItem, NavbarMenuToggle for mobile
|
|
||||||
2. Hero — large headline, descriptive subtext, Button (color="primary" size="lg") + Button (variant="bordered")
|
|
||||||
3. Social proof — Avatar group for client logos or Chip components for trust badges
|
|
||||||
4. Features — Card components in grid, each with CardHeader, CardBody, use Divider between sections
|
|
||||||
5. Product demo — Tabs component to show different product views/features interactively
|
|
||||||
6. Testimonials — Card grid with User component (avatar + name + role) inside each card
|
|
||||||
7. Pricing — Card components with Chip for "Popular" badge, Listbox for feature lists
|
|
||||||
8. FAQ — Accordion component with AccordionItem for each question
|
|
||||||
9. CTA — centered section with Input + Button combo for email capture
|
|
||||||
10. Footer — clean grid with Link components, Divider above copyright
|
|
||||||
|
|
||||||
CODE QUALITY:
|
|
||||||
- Fully responsive with HeroUI's responsive props
|
|
||||||
- Accessible by default (React Aria foundation)
|
|
||||||
- Server-side rendering compatible
|
|
||||||
- Optimized bundle (import individual components)
|
|
||||||
- TypeScript strict mode
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Chakra UI
|
|
||||||
|
|
||||||
**Stack:** React, Emotion (CSS-in-JS)
|
|
||||||
**Install:** `npm i @chakra-ui/react`
|
|
||||||
**Site:** chakra-ui.com
|
|
||||||
|
|
||||||
```
|
|
||||||
Build a clean, accessible landing page using Next.js 14+, TypeScript, and Chakra UI v3.
|
|
||||||
|
|
||||||
ARCHITECTURE:
|
|
||||||
- Next.js App Router with ChakraProvider at root
|
|
||||||
- Use Chakra's design tokens and theme extension for brand customization
|
|
||||||
- Organize: /app, /components/sections, /theme (custom theme config)
|
|
||||||
|
|
||||||
DESIGN PRINCIPLES:
|
|
||||||
- Clean and warm aesthetic using Chakra's polished defaults
|
|
||||||
- Consistent spacing using Chakra's space scale (4, 8, 12, 16, 20, 24)
|
|
||||||
- Typography: use Chakra's Heading and Text components
|
|
||||||
- Color mode support (light/dark) via useColorMode hook
|
|
||||||
- Accessible-first: every component follows WAI-ARIA by default
|
|
||||||
|
|
||||||
SECTIONS TO BUILD WITH CHAKRA COMPONENTS:
|
|
||||||
1. Navbar — Flex with HStack, IconButton for mobile menu, Button for CTA
|
|
||||||
2. Hero — VStack centered, Heading size="4xl", Text, HStack with two Buttons
|
|
||||||
3. Social proof — HStack/Wrap of grayscale Image components
|
|
||||||
4. Features — SimpleGrid columns={{base:1, md:3}} of VStack cards with Icon, Heading, Text
|
|
||||||
5. Stats — Stat component group with StatLabel, StatNumber, StatHelpText
|
|
||||||
6. Testimonials — SimpleGrid of Card with Avatar, Text (quote), Heading (name)
|
|
||||||
7. Pricing — SimpleGrid of Card, Badge for "Popular", List for features
|
|
||||||
8. FAQ — Accordion with AccordionItem, AccordionButton, AccordionPanel
|
|
||||||
9. CTA — Box with bg gradient, VStack centered, Heading, Text, Button
|
|
||||||
10. Footer — SimpleGrid columns={{base:2, md:4}} with VStack, Heading, Link
|
|
||||||
|
|
||||||
THEME EXTENSION:
|
|
||||||
const theme = extendTheme({
|
|
||||||
colors: { brand: { 50: '...', 500: '...', 900: '...' } },
|
|
||||||
fonts: { heading: 'Your Font', body: 'Your Font' },
|
|
||||||
components: { Button: { defaultProps: { colorScheme: 'brand' } } }
|
|
||||||
})
|
|
||||||
|
|
||||||
CODE QUALITY:
|
|
||||||
- Fully responsive using Chakra's responsive array/object syntax
|
|
||||||
- Dark mode ready with useColorModeValue
|
|
||||||
- Accessible by default
|
|
||||||
- TypeScript throughout with proper prop types
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Flowbite
|
|
||||||
|
|
||||||
**Stack:** Tailwind CSS (React, Vue, Svelte, Angular)
|
|
||||||
**Install:** `npm i flowbite flowbite-react`
|
|
||||||
**Site:** flowbite.com
|
|
||||||
|
|
||||||
```
|
|
||||||
Build a professional, conversion-optimized landing page using Next.js 14+, Tailwind CSS, and Flowbite React components.
|
|
||||||
|
|
||||||
ARCHITECTURE:
|
|
||||||
- Next.js App Router with Flowbite React components
|
|
||||||
- Import from flowbite-react (Button, Card, Navbar, Footer, etc.)
|
|
||||||
- Flowbite Tailwind plugin in tailwind.config for interactive styles
|
|
||||||
|
|
||||||
DESIGN PRINCIPLES:
|
|
||||||
- Business-professional aesthetic (clean, trustworthy, conversion-focused)
|
|
||||||
- Cool neutral palette (slate/gray) with strong brand accent
|
|
||||||
- Focus on readability and clear information hierarchy
|
|
||||||
|
|
||||||
SECTIONS TO BUILD WITH FLOWBITE COMPONENTS:
|
|
||||||
1. Navbar — Flowbite Navbar with Navbar.Brand, Navbar.Toggle, Navbar.Collapse
|
|
||||||
2. Hero — Jumbotron-style, large Heading, Button group
|
|
||||||
3. Social proof — row of client logos
|
|
||||||
4. Features — Card components in responsive grid with icon badge, title, description
|
|
||||||
5. Content — alternating image + text rows
|
|
||||||
6. Testimonials — Blockquote or Card with Rating component for stars
|
|
||||||
7. Pricing — Card with List for features, Badge for highlighted plan
|
|
||||||
8. FAQ — Accordion component
|
|
||||||
9. Newsletter CTA — TextInput + Button in a styled Banner
|
|
||||||
10. Footer — Flowbite Footer with Footer.LinkGroup, Footer.Brand, Footer.Copyright
|
|
||||||
|
|
||||||
CODE QUALITY:
|
|
||||||
- Fully responsive (components are responsive by default)
|
|
||||||
- Accessible (follows WAI-ARIA)
|
|
||||||
- Works server-side with Next.js
|
|
||||||
- Production-ready component patterns
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Preline UI
|
|
||||||
|
|
||||||
**Stack:** Tailwind CSS (HTML-first, React/Vue plugins)
|
|
||||||
**Install:** `npm i preline` + add plugin to tailwind.config
|
|
||||||
**Site:** preline.co
|
|
||||||
|
|
||||||
```
|
|
||||||
Build a sleek, modern landing page using Next.js 14+, Tailwind CSS, and Preline UI components.
|
|
||||||
|
|
||||||
ARCHITECTURE:
|
|
||||||
- Next.js App Router with Preline's Tailwind plugin
|
|
||||||
- Use Preline's HTML component patterns (class-based, minimal JS)
|
|
||||||
- Import preline JS for interactive components (dropdowns, modals, accordions)
|
|
||||||
|
|
||||||
DESIGN PRINCIPLES:
|
|
||||||
- Modern SaaS aesthetic (clean lines, airy spacing, professional)
|
|
||||||
- Neutral base (white/slate) with vibrant accent for CTAs
|
|
||||||
- Large section padding (py-16 to py-24)
|
|
||||||
|
|
||||||
SECTIONS TO BUILD WITH PRELINE PATTERNS:
|
|
||||||
1. Navbar — header component with mega menu support, sticky on scroll
|
|
||||||
2. Hero — centered or split layout, large headline with gradient text, two CTA buttons
|
|
||||||
3. Social proof — logo ticker or static row with grayscale filter
|
|
||||||
4. Features — icon + text cards in 3-column grid
|
|
||||||
5. Tabs showcase — tab component showing different product features per tab
|
|
||||||
6. Testimonials — grid or slider of testimonial cards
|
|
||||||
7. Pricing — pricing table pattern with toggle for monthly/annual
|
|
||||||
8. Stats — counter section with large numbers + labels
|
|
||||||
9. FAQ — accordion component with smooth expand/collapse
|
|
||||||
10. CTA + Footer — email input + button CTA above multi-column footer
|
|
||||||
|
|
||||||
CODE QUALITY:
|
|
||||||
- Fully responsive with Tailwind breakpoints
|
|
||||||
- Accessible (follows ARIA standards)
|
|
||||||
- Lightweight (CSS + minimal JS)
|
|
||||||
- Works with any framework via HTML classes
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Park UI
|
|
||||||
|
|
||||||
**Stack:** React/Vue/Solid, Ark UI (headless), Tailwind CSS
|
|
||||||
**Install:** Copy-paste components (shadcn-style)
|
|
||||||
**Site:** park-ui.com
|
|
||||||
|
|
||||||
```
|
|
||||||
Build a refined, design-system-driven landing page using Next.js 14+, TypeScript, Tailwind CSS, and Park UI components.
|
|
||||||
|
|
||||||
ARCHITECTURE:
|
|
||||||
- Next.js App Router with Park UI components (Ark UI + Tailwind CSS)
|
|
||||||
- Copy-paste component installation (like shadcn)
|
|
||||||
- Organize: /app, /components/ui (Park UI primitives), /components/sections
|
|
||||||
|
|
||||||
DESIGN PRINCIPLES:
|
|
||||||
- Polished and systematic (design-system quality)
|
|
||||||
- Neutral, sophisticated palette with deliberate accent usage
|
|
||||||
- Consistent spacing and sizing via Park UI's token scale
|
|
||||||
- Light/dark mode via Park UI's built-in theme tokens
|
|
||||||
|
|
||||||
SECTIONS TO BUILD WITH PARK UI COMPONENTS:
|
|
||||||
1. Navbar — navigation components, clean horizontal layout, mobile drawer
|
|
||||||
2. Hero — Heading + Text with proper token sizing, Button pair (solid + outline)
|
|
||||||
3. Social proof — subtle logo row with muted styling
|
|
||||||
4. Features — Card components in grid with Icon, Heading, Text
|
|
||||||
5. Tabs showcase — Tabs component for interactive product feature display
|
|
||||||
6. Testimonials — Card-based layout with Avatar + quote
|
|
||||||
7. Pricing — Card grid with Badge for recommended tier
|
|
||||||
8. FAQ — Accordion component (Ark UI primitive, styled via Park UI)
|
|
||||||
9. CTA — centered section with Input + Button
|
|
||||||
10. Footer — grid layout with Link groups and Separator
|
|
||||||
|
|
||||||
CODE QUALITY:
|
|
||||||
- Fully responsive and accessible
|
|
||||||
- Ark UI headless primitives for rock-solid accessibility
|
|
||||||
- TypeScript strict mode
|
|
||||||
- Component-per-section architecture
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Origin UI
|
|
||||||
|
|
||||||
**Stack:** React, Tailwind CSS, shadcn/ui foundation
|
|
||||||
**Install:** Copy-paste (400+ free components)
|
|
||||||
**Site:** originui.com
|
|
||||||
|
|
||||||
```
|
|
||||||
Build a polished, component-rich landing page using Next.js 14+, TypeScript, Tailwind CSS, and Origin UI components (built on shadcn/ui).
|
|
||||||
|
|
||||||
ARCHITECTURE:
|
|
||||||
- Next.js App Router with shadcn/ui base + Origin UI component variants
|
|
||||||
- Copy-paste Origin UI components into /components/ui
|
|
||||||
- Origin UI extends shadcn patterns with 400+ variant options
|
|
||||||
|
|
||||||
DESIGN PRINCIPLES:
|
|
||||||
- Professional and versatile (many style variants per component)
|
|
||||||
- Pick a cohesive subset of variants that match your brand
|
|
||||||
- Consistent spacing and typography aligned with shadcn's token system
|
|
||||||
- Focus on component quality and consistency over flashy animation
|
|
||||||
|
|
||||||
SECTIONS TO BUILD:
|
|
||||||
1. Navbar — choose from Origin UI's multiple navbar variants
|
|
||||||
2. Hero — select a hero variant (centered, split, with background pattern)
|
|
||||||
3. Social proof — logo cloud variant
|
|
||||||
4. Features — pick feature section variant (grid, list, bento, with images)
|
|
||||||
5. Content — alternating image/text sections
|
|
||||||
6. Testimonials — choose testimonial variant (cards, quotes, carousel)
|
|
||||||
7. Pricing — select pricing table variant (simple, comparison, with toggle)
|
|
||||||
8. FAQ — accordion variant
|
|
||||||
9. CTA — pick CTA variant (centered, with input, split layout)
|
|
||||||
10. Footer — choose footer variant (simple, multi-column, with newsletter)
|
|
||||||
|
|
||||||
CODE QUALITY:
|
|
||||||
- Fully responsive (all components are mobile-first)
|
|
||||||
- Accessible (shadcn/ui + Radix UI foundation)
|
|
||||||
- TypeScript throughout
|
|
||||||
- Minimal dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Headless UI
|
|
||||||
|
|
||||||
**Stack:** React or Vue, Tailwind CSS (by Tailwind Labs)
|
|
||||||
**Install:** `npm i @headlessui/react`
|
|
||||||
**Site:** headlessui.com
|
|
||||||
|
|
||||||
```
|
|
||||||
Build a custom-designed, fully accessible landing page using Next.js 14+, TypeScript, Tailwind CSS, and Headless UI for interactive components.
|
|
||||||
|
|
||||||
ARCHITECTURE:
|
|
||||||
- Next.js App Router with Tailwind CSS for all styling
|
|
||||||
- Headless UI only for interactive behavior (menus, dialogs, tabs, disclosure, transitions)
|
|
||||||
- All visual design is custom Tailwind — Headless UI provides zero styling
|
|
||||||
- Organize: /app, /components/ui (custom-styled wrappers), /components/sections
|
|
||||||
|
|
||||||
DESIGN PRINCIPLES:
|
|
||||||
- Completely custom aesthetic — you control every pixel
|
|
||||||
- Minimalist, editorial-quality design (think Stripe, Linear, Vercel)
|
|
||||||
- Monochrome base with ONE signature accent color
|
|
||||||
- Large whitespace, precise typography, meticulous alignment
|
|
||||||
- Every interactive element is keyboard-navigable and screen-reader friendly
|
|
||||||
|
|
||||||
SECTIONS TO BUILD:
|
|
||||||
1. Navbar — custom with Headless UI Popover for dropdowns, Disclosure for mobile menu
|
|
||||||
2. Hero — pure Tailwind, dramatic typography, clean CTAs
|
|
||||||
3. Social proof — pure Tailwind logo row
|
|
||||||
4. Features — custom card grid, optional Tab component for tabbed showcase
|
|
||||||
5. Product showcase — TabGroup for interactive product views
|
|
||||||
6. Testimonials — pure Tailwind card grid
|
|
||||||
7. Pricing — RadioGroup for plan selection, Switch for monthly/annual toggle
|
|
||||||
8. FAQ — Disclosure component for accessible expand/collapse
|
|
||||||
9. CTA — pure Tailwind centered section
|
|
||||||
10. Footer — pure Tailwind grid layout
|
|
||||||
|
|
||||||
HEADLESS UI COMPONENTS TO LEVERAGE:
|
|
||||||
- Popover — navbar dropdowns
|
|
||||||
- Disclosure — mobile menu, FAQ items
|
|
||||||
- Dialog — modals/overlays
|
|
||||||
- Transition — smooth enter/leave animations
|
|
||||||
- RadioGroup — plan selection
|
|
||||||
- Switch — toggles
|
|
||||||
- TabGroup — tabbed content
|
|
||||||
|
|
||||||
CODE QUALITY:
|
|
||||||
- 100% accessible (Headless UI's core guarantee)
|
|
||||||
- Fully responsive custom Tailwind design
|
|
||||||
- Keyboard navigation on all interactive elements
|
|
||||||
- TypeScript with proper generics
|
|
||||||
- Zero design dependencies — total creative control
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Cult UI
|
|
||||||
|
|
||||||
**Stack:** React, Next.js, Tailwind CSS, shadcn/ui foundation
|
|
||||||
**Install:** Copy-paste or CLI
|
|
||||||
**Site:** cult-ui.com
|
|
||||||
|
|
||||||
```
|
|
||||||
Build a modern, AI-forward landing page using Next.js 14+, TypeScript, Tailwind CSS, and Cult UI components.
|
|
||||||
|
|
||||||
ARCHITECTURE:
|
|
||||||
- Next.js App Router with Cult UI components (extends shadcn/ui)
|
|
||||||
- Full-stack ready (Cult UI includes backend-aware patterns)
|
|
||||||
- Use Cult UI's AI-specific blocks if building an AI product
|
|
||||||
|
|
||||||
DESIGN PRINCIPLES:
|
|
||||||
- Tech-forward, developer-centric aesthetic
|
|
||||||
- Dark mode primary with high-contrast accents
|
|
||||||
- Modern gradients and subtle glow effects
|
|
||||||
- Clean code-like typography (mono fonts for accents)
|
|
||||||
- Purposeful animation that communicates AI/tech sophistication
|
|
||||||
|
|
||||||
SECTIONS TO BUILD:
|
|
||||||
1. Navbar — modern floating nav with glass effect
|
|
||||||
2. Hero — bold headline with animated text effect, live demo or interactive element, gradient mesh background
|
|
||||||
3. Features — AI blocks for feature cards, animated icons or micro-interactions
|
|
||||||
4. Live demo — interactive component showing product in action
|
|
||||||
5. Code showcase — syntax-highlighted code examples using Cult UI's code block components
|
|
||||||
6. Testimonials — modern card layout with subtle animation
|
|
||||||
7. Pricing — clean tier comparison with feature matrix
|
|
||||||
8. Open source / community — GitHub stars counter, contributor avatars
|
|
||||||
9. CTA — compelling action section with email input or waitlist signup
|
|
||||||
10. Footer — developer-friendly footer with docs links, API reference, status page
|
|
||||||
|
|
||||||
CODE QUALITY:
|
|
||||||
- Full-stack patterns (API routes + client components)
|
|
||||||
- TypeScript strict mode throughout
|
|
||||||
- Accessible interactive components
|
|
||||||
- Production-ready architecture patterns
|
|
||||||
```
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"suggestions": [],
|
|
||||||
"generated_at": null
|
|
||||||
}
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"created_at": "2026-06-03T13:15:37.485Z",
|
|
||||||
"lifetime": {
|
|
||||||
"total_tokens_estimated": 21123,
|
|
||||||
"total_reads": 14,
|
|
||||||
"total_writes": 11,
|
|
||||||
"total_sessions": 4,
|
|
||||||
"anatomy_hits": 4,
|
|
||||||
"anatomy_misses": 10,
|
|
||||||
"repeated_reads_blocked": 0,
|
|
||||||
"estimated_savings_vs_bare_cli": 800
|
|
||||||
},
|
|
||||||
"sessions": [
|
|
||||||
{
|
|
||||||
"id": "session-2026-06-03-1516",
|
|
||||||
"started": "2026-06-03T13:16:36.725Z",
|
|
||||||
"ended": "2026-06-03T13:16:48.722Z",
|
|
||||||
"reads": [],
|
|
||||||
"writes": [
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/CLAUDE.md",
|
|
||||||
"tokens_estimated": 628,
|
|
||||||
"action": "create"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"totals": {
|
|
||||||
"input_tokens_estimated": 0,
|
|
||||||
"output_tokens_estimated": 628,
|
|
||||||
"reads_count": 0,
|
|
||||||
"writes_count": 1,
|
|
||||||
"repeated_reads_blocked": 0,
|
|
||||||
"anatomy_lookups": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "session-2026-06-03-1516",
|
|
||||||
"started": "2026-06-03T13:16:36.725Z",
|
|
||||||
"ended": "2026-06-03T13:17:38.452Z",
|
|
||||||
"reads": [
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/CLAUDE.md",
|
|
||||||
"tokens_estimated": 550,
|
|
||||||
"was_repeated": false,
|
|
||||||
"anatomy_had_description": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"writes": [
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/CLAUDE.md",
|
|
||||||
"tokens_estimated": 628,
|
|
||||||
"action": "create"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"totals": {
|
|
||||||
"input_tokens_estimated": 550,
|
|
||||||
"output_tokens_estimated": 628,
|
|
||||||
"reads_count": 1,
|
|
||||||
"writes_count": 1,
|
|
||||||
"repeated_reads_blocked": 0,
|
|
||||||
"anatomy_lookups": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "session-2026-06-03-1516",
|
|
||||||
"started": "2026-06-03T13:16:36.725Z",
|
|
||||||
"ended": "2026-06-03T13:30:44.458Z",
|
|
||||||
"reads": [
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/CLAUDE.md",
|
|
||||||
"tokens_estimated": 550,
|
|
||||||
"was_repeated": false,
|
|
||||||
"anatomy_had_description": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/output/index.html",
|
|
||||||
"tokens_estimated": 0,
|
|
||||||
"was_repeated": false,
|
|
||||||
"anatomy_had_description": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/output/NETgazet-07_okt2024/page-01.html",
|
|
||||||
"tokens_estimated": 0,
|
|
||||||
"was_repeated": false,
|
|
||||||
"anatomy_had_description": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/output/NETgazet-07_okt2024/index.html",
|
|
||||||
"tokens_estimated": 0,
|
|
||||||
"was_repeated": false,
|
|
||||||
"anatomy_had_description": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"writes": [
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/CLAUDE.md",
|
|
||||||
"tokens_estimated": 628,
|
|
||||||
"action": "create"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/.claude/plans/in-the-example-folder-immutable-rossum.md",
|
|
||||||
"tokens_estimated": 2414,
|
|
||||||
"action": "create"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/main.py",
|
|
||||||
"tokens_estimated": 2847,
|
|
||||||
"action": "create"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"totals": {
|
|
||||||
"input_tokens_estimated": 550,
|
|
||||||
"output_tokens_estimated": 5889,
|
|
||||||
"reads_count": 4,
|
|
||||||
"writes_count": 3,
|
|
||||||
"repeated_reads_blocked": 0,
|
|
||||||
"anatomy_lookups": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "session-2026-06-03-1516",
|
|
||||||
"started": "2026-06-03T13:16:36.725Z",
|
|
||||||
"ended": "2026-06-03T13:34:03.911Z",
|
|
||||||
"reads": [
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/CLAUDE.md",
|
|
||||||
"tokens_estimated": 550,
|
|
||||||
"was_repeated": false,
|
|
||||||
"anatomy_had_description": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/output/index.html",
|
|
||||||
"tokens_estimated": 0,
|
|
||||||
"was_repeated": false,
|
|
||||||
"anatomy_had_description": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/output/NETgazet-07_okt2024/page-01.html",
|
|
||||||
"tokens_estimated": 0,
|
|
||||||
"was_repeated": false,
|
|
||||||
"anatomy_had_description": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/output/NETgazet-07_okt2024/index.html",
|
|
||||||
"tokens_estimated": 0,
|
|
||||||
"was_repeated": false,
|
|
||||||
"anatomy_had_description": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"writes": [
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/CLAUDE.md",
|
|
||||||
"tokens_estimated": 628,
|
|
||||||
"action": "create"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/.claude/plans/in-the-example-folder-immutable-rossum.md",
|
|
||||||
"tokens_estimated": 2414,
|
|
||||||
"action": "create"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/main.py",
|
|
||||||
"tokens_estimated": 2847,
|
|
||||||
"action": "create"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"totals": {
|
|
||||||
"input_tokens_estimated": 550,
|
|
||||||
"output_tokens_estimated": 5889,
|
|
||||||
"reads_count": 4,
|
|
||||||
"writes_count": 3,
|
|
||||||
"repeated_reads_blocked": 0,
|
|
||||||
"anatomy_lookups": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "session-2026-06-03-1516",
|
|
||||||
"started": "2026-06-03T13:16:36.725Z",
|
|
||||||
"ended": "2026-06-03T13:35:12.255Z",
|
|
||||||
"reads": [
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/CLAUDE.md",
|
|
||||||
"tokens_estimated": 550,
|
|
||||||
"was_repeated": false,
|
|
||||||
"anatomy_had_description": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/output/index.html",
|
|
||||||
"tokens_estimated": 0,
|
|
||||||
"was_repeated": false,
|
|
||||||
"anatomy_had_description": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/output/NETgazet-07_okt2024/page-01.html",
|
|
||||||
"tokens_estimated": 0,
|
|
||||||
"was_repeated": false,
|
|
||||||
"anatomy_had_description": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/output/NETgazet-07_okt2024/index.html",
|
|
||||||
"tokens_estimated": 0,
|
|
||||||
"was_repeated": false,
|
|
||||||
"anatomy_had_description": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"writes": [
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/CLAUDE.md",
|
|
||||||
"tokens_estimated": 628,
|
|
||||||
"action": "create"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/.claude/plans/in-the-example-folder-immutable-rossum.md",
|
|
||||||
"tokens_estimated": 2414,
|
|
||||||
"action": "create"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/main.py",
|
|
||||||
"tokens_estimated": 2847,
|
|
||||||
"action": "create"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"totals": {
|
|
||||||
"input_tokens_estimated": 550,
|
|
||||||
"output_tokens_estimated": 5889,
|
|
||||||
"reads_count": 4,
|
|
||||||
"writes_count": 3,
|
|
||||||
"repeated_reads_blocked": 0,
|
|
||||||
"anatomy_lookups": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "session-2026-06-03-1555",
|
|
||||||
"started": "2026-06-03T13:55:38.248Z",
|
|
||||||
"ended": "2026-06-03T13:56:28.840Z",
|
|
||||||
"reads": [
|
|
||||||
{
|
|
||||||
"file": "/home/seppe4/vscode_projects/netgazet/output/index.html",
|
|
||||||
"tokens_estimated": 0,
|
|
||||||
"was_repeated": false,
|
|
||||||
"anatomy_had_description": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"writes": [],
|
|
||||||
"totals": {
|
|
||||||
"input_tokens_estimated": 0,
|
|
||||||
"output_tokens_estimated": 0,
|
|
||||||
"reads_count": 1,
|
|
||||||
"writes_count": 0,
|
|
||||||
"repeated_reads_blocked": 0,
|
|
||||||
"anatomy_lookups": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"daemon_usage": [],
|
|
||||||
"waste_flags": [],
|
|
||||||
"optimization_report": {
|
|
||||||
"last_generated": null,
|
|
||||||
"patterns": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user