Marketing
Hook
Hooks
What peec-ai-skills runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
$ npx -y skills add AntonioBlago/peec-ai-skills --agent claude-codeShips with peec-ai-skills. Installing the plugin gets these hooks.
Where it lives
- hooks/peec-detect.pyGitHub
Read the script
#!/usr/bin/env python3 """ peec-detect — shared Peec project detector for the peec-ai-skills hook + /peec-start. Reads: - $CLAUDE_PROJECT_DIR or cwd as the project root - <root>/growth_loop/setup_state.json (and up to 2 parent levels) - On UserPromptSubmit: stdin JSON {hook_event_name, prompt, ...} — keyword-filtered Writes to stdout (becomes additionalContext for Claude): A short status block + a recommended next-action. Silent if neither a state file nor Peec-trigger keywords are present (so non-Peec sessions are not polluted). Exit codes: 0 — always (never block the user) Usage in ~/.claude/settings.json: { "hooks": { "UserPromptSubmit": [ { "matcher": "", "hooks": [ { "type": "command", "command": "python ~/PycharmProjects/peec-ai-skills/hooks/peec-detect.py" } ] } ], "SessionStart": [ { "hooks": [ { "type": "command", "command": "python ~/PycharmProjects/peec-ai-skills/hooks/peec-detect.py --session-start" } ] } ] } } """ from __future__ import annotations import json import os import sys from datetime import datetime, timezone from pathlib import Path from typing import Any # Force UTF-8 stdout so Unicode arrows and Umlauts survive Windows cp1252 consoles. try: sys.stdout.reconfigure(encoding="utf-8", errors="replace") except (AttributeError, OSError): pass TRIGGER_KEYWORDS = { # English "peec", "ai visibility", "ai-visibility", "growth loop", "growth-loop", "citation outreach", "content cluster", "ai search", "llm visibility", # German "peec ai", "sichtbarkeit", "ki-sichtbarkeit", "ki sichtbarkeit", "ai-sichtbarkeit", "ai sichtbarkeit", "wachstumsschleife", # Slash-style mentions (helps when user types "@peec" or refers to skills) "/peec-setup", "/peec-agent", "/peec-content-intel", "/peec-cluster", "/peec-outreach", "/peec-report", "/peec-start", } STATE_FILENAME = "setup_state.json" STATE_DIR = "growth_loop" MAX_PARENT_LEVELS = 2 def find_state_file(start: Path) -> Path | None: """Walk up at most MAX_PARENT_LEVELS looking for growth_loop/setup_state.json.""" cur = start.resolve() for _ in range(MAX_PARENT_LEVELS + 1): candidate = cur / STATE_DIR / STATE_FILENAME if candidate.is_file(): return candidate if cur.parent == cur: break cur = cur.parent return None def parse_iso8601(value: str | None) -> datetime | None: if not value: return None try: # Accept trailing Z by replacing it with +00:00 if value.endswith("Z"): value = value[:-1] + "+00:00" dt = datetime.fromisoformat(value) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt except ValueError: return None def load_state(path: Path) -> dict[str, Any] | None: try: return json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return None def age_days(dt: datetime | None) -> int | None: if dt is None: return None return (datetime.now(timezone.utc) - dt).days def classify_age(days: int | None) -> str: if days is None: return "unknown" if days < 30: return "fresh" if days < 90: return "stale" return "very stale" def build_state_summary(state: dict[str, Any], state_path: Path) -> str: completed_at = parse_iso8601(state.get("completed_at")) days = age_days(completed_at) age_label = classify_age(days) snapshot = state.get("snapshot") or {} phases = state.get("phases_completed") or [] project_id = state.get("peec_project_id", "?") domain = state.get("domain", "?") lang = state.get("prompt_language", "?") country = state.get("target_country", "?") next_action = recommend_action(age_label, phases) lines = [ "=== PEEC PROJECT DETECTED ===", f"State file: {state_path}", f"Project: {domain} (peec_project_id={project_id})", f"Locale: country={country} · language={lang}", f"Setup age: {days if days is not None else '?'} days ({age_label})", f"Phases completed: {', '.join(phases) if phases else '—'}", f"Snapshot: brands={snapshot.get('brands','?')} · " f"prompts={snapshot.get('prompts','?')} · topics={snapshot.get('topics','?')} · " f"tags={snapshot.get('tags','?')}", "", f"RECOMMENDED NEXT ACTION: {next_action}", "If the user is here for Peec growth work, immediately invoke the recommended skill.", "============================", ] return "\n".join(lines) def recommend_action(age_label: str, phases: list[str]) -> str: required = {"competitors", "prompts", "topics", "tags"} missing = required - set(phases or []) if missing: return ( f"/peec-setup partial:{sorted(missing)[0]} " f"— missing phases: {sorted(missing)}" ) if age_label == "very stale": return "/peec-setup audit — setup older than 90 days" if age_label == "stale": return "/peec-agent (then consider /peec-setup audit if drift detected)" return "/peec-agent — pick the single next move for this cycle" def build_no_state_hint(reason: str) -> str: return "\n".join([ "=== PEEC CONTEXT ===", f"No setup_state.json found (reason: {reason}).", "", "RECOMMENDED NEXT ACTION: /peec-start", " → if Peec project exists with brands/prompts → import mode (no destructive setup)", " → if greenfield → run /peec-setup full", "===================", ]) def is_session_start(argv: list[str]) -> bool: return "--session-start" in argv def read_stdin_event() -> dict[str, Any] | None:
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withpeec-ai-skills
Production-tested Claude Code skills for Peec AI — the brand-visibility tracking platform for LLM-powered search (ChatGPT, Perplexity, Google AI Overviews, Gemini).
Get the whole plugin
Stats
9
Stars
0
Forks
Maintained
Maintenance
Python
Language
MIT
License
5mo ago
Last commit
5mo ago
Created
Repo: AntonioBlago/peec-ai-skills

