← back to heg-wtf__cclaw

Function bodies 139 total

All specs Real LLM only Function bodies
get_claude_session_id function · python · L126-L131 (6 LOC)
src/cclaw/session.py
def get_claude_session_id(session_directory: Path) -> str | None:
    """Read stored Claude Code session ID."""
    path = session_directory / CLAUDE_SESSION_ID_FILE
    if path.exists():
        return path.read_text().strip()
    return None
load_conversation_history function · python · L144-L190 (47 LOC)
src/cclaw/session.py
def load_conversation_history(
    session_directory: Path, max_turns: int = MAX_CONVERSATION_HISTORY_TURNS
) -> str | None:
    """Read last N turns from conversation files.

    Searches conversation-YYMMDD.md files from newest to oldest,
    collecting turns until max_turns is reached.
    Falls back to legacy conversation.md if no dated files exist.

    Returns None if no conversation files exist or all are empty.
    """
    # Dated files in reverse chronological order (newest first)
    conversation_files = sorted(
        session_directory.glob(CONVERSATION_GLOB_PATTERN),
        reverse=True,
    )

    # Legacy fallback
    if not conversation_files:
        legacy_file = session_directory / "conversation.md"
        if legacy_file.exists():
            conversation_files = [legacy_file]

    if not conversation_files:
        return None

    all_sections: list[str] = []

    for conversation_file in conversation_files:
        content = conversation_file.read_text()
       
log_conversation function · python · L193-L209 (17 LOC)
src/cclaw/session.py
def log_conversation(session_directory: Path, role: str, content: str) -> None:
    """Append a conversation entry to today's conversation file.

    Writes to conversation-YYMMDD.md (UTC date).

    Args:
        session_directory: Path to the session directory.
        role: Either 'user' or 'assistant'.
        content: The message content.
    """
    conversation_file = _conversation_file_for_today(session_directory)
    timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")

    entry = f"\n## {role} ({timestamp})\n\n{content}\n"

    with open(conversation_file, "a") as file:
        file.write(entry)
list_workspace_files function · python · L212-L225 (14 LOC)
src/cclaw/session.py
def list_workspace_files(session_directory: Path) -> list[str]:
    """List all files in the session's workspace directory.

    Returns a list of relative file paths within the workspace.
    """
    workspace = session_directory / "workspace"
    if not workspace.exists():
        return []

    files = []
    for file_path in sorted(workspace.rglob("*")):
        if file_path.is_file():
            files.append(str(file_path.relative_to(workspace)))
    return files
load_bot_memory function · python · L236-L247 (12 LOC)
src/cclaw/session.py
def load_bot_memory(bot_path: Path) -> str | None:
    """Load the bot's MEMORY.md content.

    Returns None if MEMORY.md doesn't exist or is empty.
    """
    path = memory_file_path(bot_path)
    if not path.exists():
        return None
    content = path.read_text()
    if not content.strip():
        return None
    return content
load_global_memory function · python · L270-L281 (12 LOC)
src/cclaw/session.py
def load_global_memory() -> str | None:
    """Load the global GLOBAL_MEMORY.md content.

    Returns None if GLOBAL_MEMORY.md doesn't exist or is empty.
    """
    path = global_memory_file_path()
    if not path.exists():
        return None
    content = path.read_text()
    if not content.strip():
        return None
    return content
save_global_memory function · python · L284-L289 (6 LOC)
src/cclaw/session.py
def save_global_memory(content: str) -> None:
    """Save content to the global GLOBAL_MEMORY.md file."""
    from cclaw.config import ensure_home

    ensure_home()
    global_memory_file_path().write_text(content)
If a scraper extracted this row, it came from Repobility (https://repobility.com)
list_skills function · python · L38-L71 (34 LOC)
src/cclaw/skill.py
def list_skills() -> list[dict[str, Any]]:
    """List all recognized skills (directories containing SKILL.md)."""
    directory = skills_directory()
    if not directory.exists():
        return []

    from cclaw.builtin_skills import get_builtin_skill_path

    result = []
    for entry in sorted(directory.iterdir()):
        if entry.is_dir() and (entry / "SKILL.md").exists():
            config = load_skill_config(entry.name)
            emoji = config.get("emoji", "") if config else ""

            # Fallback: read emoji from builtin template if not in installed config
            if not emoji:
                builtin_path = get_builtin_skill_path(entry.name)
                if builtin_path:
                    builtin_yaml_path = builtin_path / "skill.yaml"
                    if builtin_yaml_path.exists():
                        with open(builtin_yaml_path) as builtin_file:
                            builtin_config = yaml.safe_load(builtin_file) or {}
                        
load_skill_config function · python · L79-L85 (7 LOC)
src/cclaw/skill.py
def load_skill_config(name: str) -> dict[str, Any] | None:
    """Load a skill's skill.yaml. Returns None if it doesn't exist."""
    path = skill_directory(name) / "skill.yaml"
    if not path.exists():
        return None
    with open(path) as file:
        return yaml.safe_load(file) or {}
load_skill_markdown function · python · L95-L100 (6 LOC)
src/cclaw/skill.py
def load_skill_markdown(name: str) -> str | None:
    """Load a skill's SKILL.md content. Returns None if it doesn't exist."""
    path = skill_directory(name) / "SKILL.md"
    if not path.exists():
        return None
    return path.read_text()
skill_status function · python · L103-L113 (11 LOC)
src/cclaw/skill.py
def skill_status(name: str) -> str:
    """Return the status of a skill: active, inactive, or not_found."""
    if not is_skill(name):
        return "not_found"

    config = load_skill_config(name)
    if config is None:
        # Markdown-only skill (no skill.yaml) is always active
        return "active"

    return config.get("status", "inactive")
skill_type function · python · L116-L121 (6 LOC)
src/cclaw/skill.py
def skill_type(name: str) -> str | None:
    """Return the skill type. None means markdown-only (no tools)."""
    config = load_skill_config(name)
    if config is None:
        return None
    return config.get("type")
generate_skill_markdown function · python · L134-L143 (10 LOC)
src/cclaw/skill.py
def generate_skill_markdown(name: str, description: str = "") -> str:
    """Generate default SKILL.md content."""
    return f"""# {name}

{description}

## Instructions

(Describe what this skill does and how the bot should use it.)
"""
default_skill_yaml function · python · L146-L165 (20 LOC)
src/cclaw/skill.py
def default_skill_yaml(
    name: str,
    description: str = "",
    skill_type: str | None = None,
    required_commands: list[str] | None = None,
    environment_variables: list[str] | None = None,
) -> dict[str, Any]:
    """Return a default skill.yaml structure."""
    config: dict[str, Any] = {
        "name": name,
        "description": description,
        "status": "inactive",
    }
    if skill_type:
        config["type"] = skill_type
    if required_commands:
        config["required_commands"] = required_commands
    if environment_variables:
        config["environment_variables"] = environment_variables
    return config
remove_skill function · python · L168-L183 (16 LOC)
src/cclaw/skill.py
def remove_skill(name: str) -> bool:
    """Remove a skill directory entirely.

    Also detaches it from all bots that use it.
    Returns True if the skill was found and removed.
    """
    directory = skill_directory(name)
    if not directory.exists():
        return False

    # Detach from all bots using this skill
    for bot_name in bots_using_skill(name):
        detach_skill_from_bot(bot_name, name)

    shutil.rmtree(directory)
    return True
Source: Repobility analyzer · https://repobility.com
check_skill_requirements function · python · L189-L212 (24 LOC)
src/cclaw/skill.py
def check_skill_requirements(name: str) -> list[str]:
    """Check if skill requirements are met.

    Returns a list of error messages. Empty list means all OK.
    """
    errors: list[str] = []
    config = load_skill_config(name)
    if config is None:
        return errors  # Markdown-only, no requirements

    install_hints = config.get("install_hints", {})
    for command in config.get("required_commands", []):
        if not shutil.which(command):
            hint = install_hints.get(command)
            if hint:
                errors.append(f"Required command not found: {command}\n    Install: {hint}")
            else:
                errors.append(f"Required command not found: {command}")

    for variable in config.get("environment_variables", []):
        # Only check if the variable key is defined, value can be set during setup
        pass

    return errors
activate_skill function · python · L215-L221 (7 LOC)
src/cclaw/skill.py
def activate_skill(name: str) -> None:
    """Set a skill's status to active."""
    config = load_skill_config(name)
    if config is None:
        return
    config["status"] = "active"
    save_skill_config(name, config)
get_bot_skills function · python · L227-L232 (6 LOC)
src/cclaw/skill.py
def get_bot_skills(bot_name: str) -> list[str]:
    """Get the list of skill names attached to a bot."""
    bot_config = load_bot_config(bot_name)
    if not bot_config:
        return []
    return bot_config.get("skills", [])
attach_skill_to_bot function · python · L235-L245 (11 LOC)
src/cclaw/skill.py
def attach_skill_to_bot(bot_name: str, skill_name: str) -> None:
    """Attach a skill to a bot and regenerate CLAUDE.md."""
    bot_config = load_bot_config(bot_name)
    if not bot_config:
        return

    skills = bot_config.get("skills", [])
    if skill_name not in skills:
        skills.append(skill_name)
        bot_config["skills"] = skills
        save_bot_config(bot_name, bot_config)
detach_skill_from_bot function · python · L248-L258 (11 LOC)
src/cclaw/skill.py
def detach_skill_from_bot(bot_name: str, skill_name: str) -> None:
    """Detach a skill from a bot and regenerate CLAUDE.md."""
    bot_config = load_bot_config(bot_name)
    if not bot_config:
        return

    skills = bot_config.get("skills", [])
    if skill_name in skills:
        skills.remove(skill_name)
        bot_config["skills"] = skills
        save_bot_config(bot_name, bot_config)
bots_using_skill function · python · L261-L275 (15 LOC)
src/cclaw/skill.py
def bots_using_skill(skill_name: str) -> list[str]:
    """Return a list of bot names that have this skill attached."""
    from cclaw.config import load_config

    config = load_config()
    if not config or not config.get("bots"):
        return []

    result = []
    for bot_entry in config["bots"]:
        bot_name = bot_entry["name"]
        bot_config = load_bot_config(bot_name)
        if bot_config and skill_name in bot_config.get("skills", []):
            result.append(bot_name)
    return result
compose_claude_md function · python · L281-L357 (77 LOC)
src/cclaw/skill.py
def compose_claude_md(
    bot_name: str,
    personality: str,
    description: str,
    skill_names: list[str] | None = None,
    bot_path: Path | None = None,
) -> str:
    """Compose a full CLAUDE.md combining bot profile and skill content.

    When skill_names is empty or None, output is identical to generate_claude_md().
    When bot_path is provided, a Memory section is appended with instructions
    for the bot to save and retrieve long-term memories.
    """
    sections = [
        f"# {bot_name}",
        "",
        "## Personality",
        personality,
        "",
        "## Role",
        description,
        "",
        "## Rules",
        "- Respond in Korean.",
        "- Save generated files to the workspace/ directory.",
        "- Always ask for confirmation before executing dangerous commands "
        "(delete, restart, etc.).",
        "- **절대로 Markdown 표(table)를 사용하지 마라.** "
        "Telegram에서 표는 깨진다. "
        "대신 이모지 + 한 줄씩 나열하라. "
        "예시:\n"
        
regenerate_bot_claude_md function · python · L360-L375 (16 LOC)
src/cclaw/skill.py
def regenerate_bot_claude_md(bot_name: str) -> None:
    """Regenerate a bot's CLAUDE.md based on current bot.yaml (including skills)."""
    bot_config = load_bot_config(bot_name)
    if not bot_config:
        return

    directory = bot_directory(bot_name)
    content = compose_claude_md(
        bot_name=bot_name,
        personality=bot_config.get("personality", ""),
        description=bot_config.get("description", ""),
        skill_names=bot_config.get("skills", []),
        bot_path=directory,
    )
    claude_md_path = directory / "CLAUDE.md"
    claude_md_path.write_text(content)
Powered by Repobility — scan your code at https://repobility.com
update_session_claude_md function · python · L378-L392 (15 LOC)
src/cclaw/skill.py
def update_session_claude_md(bot_path: Path) -> None:
    """Propagate the bot's CLAUDE.md to all existing sessions."""
    bot_claude_md = bot_path / "CLAUDE.md"
    if not bot_claude_md.exists():
        return

    content = bot_claude_md.read_text()
    sessions_directory = bot_path / "sessions"
    if not sessions_directory.exists():
        return

    for session in sessions_directory.iterdir():
        if session.is_dir():
            session_claude_md = session / "CLAUDE.md"
            session_claude_md.write_text(content)
load_skill_mcp_config function · python · L398-L404 (7 LOC)
src/cclaw/skill.py
def load_skill_mcp_config(name: str) -> dict[str, Any] | None:
    """Load MCP configuration from a skill's mcp.json."""
    path = skill_directory(name) / "mcp.json"
    if not path.exists():
        return None
    with open(path) as file:
        return json.load(file)
merge_mcp_configs function · python · L407-L422 (16 LOC)
src/cclaw/skill.py
def merge_mcp_configs(skill_names: list[str]) -> dict[str, Any] | None:
    """Merge MCP configurations from multiple skills.

    Returns a combined mcpServers dict, or None if no skills have MCP config.
    """
    merged_servers: dict[str, Any] = {}

    for skill_name in skill_names:
        mcp_config = load_skill_mcp_config(skill_name)
        if mcp_config and "mcpServers" in mcp_config:
            merged_servers.update(mcp_config["mcpServers"])

    if not merged_servers:
        return None

    return {"mcpServers": merged_servers}
install_builtin_skill function · python · L425-L454 (30 LOC)
src/cclaw/skill.py
def install_builtin_skill(name: str) -> Path:
    """Install a built-in skill template to the user's skills directory.

    Copies all template files (SKILL.md, skill.yaml, etc.) from the
    built-in skill package to ~/.cclaw/skills/<name>/.

    Raises:
        ValueError: If the name is not a recognized built-in skill.
        FileExistsError: If the skill is already installed.

    Returns:
        The path to the installed skill directory.
    """
    from cclaw.builtin_skills import get_builtin_skill_path

    template_path = get_builtin_skill_path(name)
    if template_path is None:
        raise ValueError(f"Unknown built-in skill: {name}")

    target = skill_directory(name)
    if target.exists():
        raise FileExistsError(f"Skill '{name}' is already installed at {target}")

    target.mkdir(parents=True)
    for source_file in template_path.iterdir():
        if source_file.is_file():
            shutil.copy2(source_file, target / source_file.name)

    logger.info("Inst
collect_skill_allowed_tools function · python · L457-L473 (17 LOC)
src/cclaw/skill.py
def collect_skill_allowed_tools(skill_names: list[str]) -> list[str]:
    """Collect allowed_tools from all specified skills.

    Returns a merged list of tool patterns for --allowedTools flag.
    """
    result: list[str] = []

    for skill_name in skill_names:
        config = load_skill_config(skill_name)
        if not config:
            continue

        tools = config.get("allowed_tools", [])
        if tools:
            result.extend(tools)

    return result
collect_skill_environment_variables function · python · L476-L492 (17 LOC)
src/cclaw/skill.py
def collect_skill_environment_variables(skill_names: list[str]) -> dict[str, str]:
    """Collect environment variables from all specified skills.

    Returns a merged dict of environment variable name → value.
    """
    result: dict[str, str] = {}

    for skill_name in skill_names:
        config = load_skill_config(skill_name)
        if not config:
            continue

        env_values = config.get("environment_variable_values", {})
        if env_values:
            result.update(env_values)

    return result
collect_compact_targets function · python · L68-L134 (67 LOC)
src/cclaw/token_compact.py
def collect_compact_targets(bot_name: str) -> list[CompactTarget]:
    """Collect all files eligible for compaction.

    Targets: MEMORY.md, user-created SKILL.md (not builtins), HEARTBEAT.md.
    """
    from cclaw.builtin_skills import is_builtin_skill
    from cclaw.skill import skill_directory

    targets: list[CompactTarget] = []

    bot_path = bot_directory(bot_name)
    bot_config = load_bot_config(bot_name)
    if not bot_config:
        return targets

    # 1. MEMORY.md
    memory_path = bot_path / "MEMORY.md"
    if memory_path.exists():
        content = memory_path.read_text()
        if content.strip():
            targets.append(
                CompactTarget(
                    label="MEMORY.md",
                    file_path=memory_path,
                    content=content,
                    line_count=len(content.splitlines()),
                    token_count=estimate_token_count(content),
                    document_type=DOCUMENT_TYPE_MEMORY,
                )
compact_content function · python · L137-L159 (23 LOC)
src/cclaw/token_compact.py
async def compact_content(
    content: str,
    document_type: str,
    working_directory: str,
    model: str = "sonnet",
    timeout: int = 120,
) -> str:
    """Compress a single document using Claude Code.

    Returns the compacted content string.
    """
    from cclaw.claude_runner import run_claude

    prompt = COMPACT_PROMPT.format(document_type=document_type, content=content)

    result = await run_claude(
        working_directory=working_directory,
        message=prompt,
        model=model,
        timeout=timeout,
    )

    return result.strip()
Repobility (the analyzer behind this table) · https://repobility.com
run_compact function · python · L162-L196 (35 LOC)
src/cclaw/token_compact.py
async def run_compact(bot_name: str, model: str = "sonnet") -> list[CompactResult]:
    """Run compaction on all eligible targets for a bot.

    Processes targets sequentially. Individual failures do not stop remaining targets.
    """
    targets = collect_compact_targets(bot_name)
    results: list[CompactResult] = []

    for target in targets:
        try:
            with tempfile.TemporaryDirectory() as temporary_directory:
                compacted = await compact_content(
                    content=target.content,
                    document_type=target.document_type,
                    working_directory=temporary_directory,
                    model=model,
                )
            results.append(
                CompactResult(
                    target=target,
                    compacted_content=compacted,
                    compacted_lines=len(compacted.splitlines()),
                    compacted_tokens=estimate_token_count(compacted),
                )
        
format_compact_report function · python · L199-L241 (43 LOC)
src/cclaw/token_compact.py
def format_compact_report(bot_name: str, results: list[CompactResult]) -> str:
    """Generate a human-readable compaction report."""
    lines = [f"\U0001f4e6 Token Compact: {bot_name}", ""]

    total_before = 0
    total_after = 0

    for result in results:
        if result.error:
            lines.append(f"\u274c {result.target.label}")
            lines.append(f"  Error: {result.error}")
            lines.append("")
            continue

        before_tokens = result.target.token_count
        after_tokens = result.compacted_tokens
        saved_tokens = before_tokens - after_tokens
        percentage = result.savings_percentage

        total_before += before_tokens
        total_after += after_tokens

        if "MEMORY" in result.target.label:
            icon = "\U0001f4dd"
        elif "Skill" in result.target.label:
            icon = "\U0001f9e9"
        else:
            icon = "\U0001f493"

        lines.append(f"{icon} {result.target.label}")
        lines.append(f"  
save_compact_results function · python · L244-L253 (10 LOC)
src/cclaw/token_compact.py
def save_compact_results(results: list[CompactResult]) -> None:
    """Save successfully compacted content back to original files.

    Only writes results that have no error.
    """
    for result in results:
        if result.error:
            continue
        result.target.file_path.write_text(result.compacted_content)
        logger.info("Saved compacted %s (%s)", result.target.label, result.target.file_path)
prompt_input function · python · L15-L25 (11 LOC)
src/cclaw/utils.py
def prompt_input(label: str, default: str | None = None) -> str:
    """Prompt for single-line input using builtin input() for IME compatibility."""
    from rich.console import Console

    if default is not None:
        Console().print(f"{label} [dim](default: {default})[/dim] ", end="")
        value = input().strip()
        return value if value else default
    else:
        Console().print(f"{label} ", end="")
        return input().strip()
prompt_multiline function · python · L28-L39 (12 LOC)
src/cclaw/utils.py
def prompt_multiline(label: str) -> str:
    """Prompt for multi-line input. Empty line finishes input."""
    from rich.console import Console

    Console().print(f"{label} [dim](empty line to finish)[/dim]")
    lines = []
    while True:
        line = input()
        if line == "":
            break
        lines.append(line)
    return "\n".join(lines).strip()
split_message function · python · L42-L63 (22 LOC)
src/cclaw/utils.py
def split_message(text: str, limit: int = TELEGRAM_MESSAGE_LIMIT) -> list[str]:
    """Split a long message into chunks that fit Telegram's message limit.

    Tries to split at newline boundaries when possible.
    """
    if len(text) <= limit:
        return [text]

    chunks = []
    while text:
        if len(text) <= limit:
            chunks.append(text)
            break

        split_index = text.rfind("\n", 0, limit)
        if split_index == -1 or split_index < limit // 2:
            split_index = limit

        chunks.append(text[:split_index])
        text = text[split_index:].lstrip("\n")

    return chunks
markdown_to_telegram_html function · python · L66-L103 (38 LOC)
src/cclaw/utils.py
def markdown_to_telegram_html(text: str) -> str:
    """Convert Markdown formatting to Telegram-compatible HTML.

    Handles: **bold**, *italic*, `code`, ```code blocks```, [links](url)
    """
    # Extract links before escaping so URLs stay intact
    link_placeholder = {}
    link_counter = 0

    def _replace_link(match: re.Match) -> str:
        nonlocal link_counter
        placeholder = f"\x00LINK{link_counter}\x00"
        link_placeholder[placeholder] = (match.group(1), match.group(2))
        link_counter += 1
        return placeholder

    text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", _replace_link, text)

    text = html.escape(text)

    text = re.sub(r"```(\w*)\n(.*?)```", r"<pre>\2</pre>", text, flags=re.DOTALL)
    text = re.sub(r"```(.*?)```", r"<pre>\1</pre>", text, flags=re.DOTALL)

    text = re.sub(r"`([^`]+)`", r"<code>\1</code>", text)

    # Headings → bold
    text = re.sub(r"^#{1,6}\s+(.+)$", r"<b>\1</b>", text, flags=re.MULTILINE)

    text = re.sub(r"\*\*(.+?)
setup_logging function · python · L106-L121 (16 LOC)
src/cclaw/utils.py
def setup_logging(log_level: str = "INFO") -> None:
    """Configure logging with daily rotation to ~/.cclaw/logs/."""
    log_directory = cclaw_home() / "logs"
    log_directory.mkdir(parents=True, exist_ok=True)

    today = datetime.now().strftime("%y%m%d")
    log_file = log_directory / f"cclaw-{today}.log"

    logging.basicConfig(
        level=getattr(logging, log_level.upper(), logging.INFO),
        format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
        handlers=[
            logging.FileHandler(log_file),
            logging.StreamHandler(),
        ],
    )
‹ prevpage 3 / 3