← back to heg-wtf__cclaw

Function bodies 139 total

All specs Real LLM only Function bodies
heartbeat_edit function · python · L1320-L1346 (27 LOC)
src/cclaw/cli.py
def heartbeat_edit(bot: str = typer.Argument(help="Bot name")) -> None:
    """Edit HEARTBEAT.md for a bot."""
    import os
    import subprocess

    from rich.console import Console

    from cclaw.config import load_bot_config
    from cclaw.heartbeat import (
        default_heartbeat_content,
        heartbeat_session_directory,
    )

    console = Console()

    if not load_bot_config(bot):
        console.print(f"[red]Bot '{bot}' not found.[/red]")
        raise typer.Exit(1)

    session_directory = heartbeat_session_directory(bot)
    heartbeat_md_path = session_directory / "HEARTBEAT.md"

    if not heartbeat_md_path.exists():
        heartbeat_md_path.write_text(default_heartbeat_content())

    editor = os.environ.get("EDITOR", "vi")
    subprocess.run([editor, str(heartbeat_md_path)])
load_config function · python · L29-L35 (7 LOC)
src/cclaw/config.py
def load_config() -> dict[str, Any] | None:
    """Load the global config.yaml. Returns None if it doesn't exist."""
    path = config_path()
    if not path.exists():
        return None
    with open(path) as file:
        return yaml.safe_load(file)
load_bot_config function · python · L50-L56 (7 LOC)
src/cclaw/config.py
def load_bot_config(name: str) -> dict[str, Any] | None:
    """Load a bot's bot.yaml. Returns None if it doesn't exist."""
    path = bot_directory(name) / "bot.yaml"
    if not path.exists():
        return None
    with open(path) as file:
        return yaml.safe_load(file)
save_bot_config function · python · L59-L81 (23 LOC)
src/cclaw/config.py
def save_bot_config(name: str, bot_config: dict[str, Any]) -> None:
    """Save a bot's bot.yaml and generate CLAUDE.md."""
    directory = bot_directory(name)
    directory.mkdir(parents=True, exist_ok=True)

    sessions_directory = directory / "sessions"
    sessions_directory.mkdir(exist_ok=True)

    with open(directory / "bot.yaml", "w") as file:
        yaml.dump(bot_config, file, default_flow_style=False, allow_unicode=True)

    # Lazy import to avoid circular dependency with skill module
    from cclaw.skill import compose_claude_md

    claude_md_content = compose_claude_md(
        bot_name=name,
        personality=bot_config.get("personality", ""),
        description=bot_config.get("description", ""),
        skill_names=bot_config.get("skills", []),
        bot_path=directory,
    )
    with open(directory / "CLAUDE.md", "w") as file:
        file.write(claude_md_content)
generate_claude_md function · python · L84-L98 (15 LOC)
src/cclaw/config.py
def generate_claude_md(name: str, personality: str, description: str) -> str:
    """Generate CLAUDE.md content for a bot."""
    return f"""# {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.).
"""
default_config function · python · L101-L109 (9 LOC)
src/cclaw/config.py
def default_config() -> dict[str, Any]:
    """Return a default config structure."""
    return {
        "bots": [],
        "settings": {
            "log_level": "INFO",
            "command_timeout": 300,
        },
    }
add_bot_to_config function · python · L112-L122 (11 LOC)
src/cclaw/config.py
def add_bot_to_config(name: str) -> None:
    """Add a bot entry to the global config."""
    config = load_config() or default_config()
    if "bots" not in config:
        config["bots"] = []

    existing = [b for b in config["bots"] if b["name"] == name]
    if not existing:
        config["bots"].append({"name": name, "path": str(bot_directory(name))})

    save_config(config)
Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
remove_bot_from_config function · python · L125-L131 (7 LOC)
src/cclaw/config.py
def remove_bot_from_config(name: str) -> None:
    """Remove a bot entry from the global config."""
    config = load_config()
    if not config or "bots" not in config:
        return
    config["bots"] = [b for b in config["bots"] if b["name"] != name]
    save_config(config)
load_cron_config function · python · L134-L143 (10 LOC)
src/cclaw/config.py
def load_cron_config(name: str) -> dict[str, Any]:
    """Load a bot's cron.yaml. Returns empty config if not found."""
    path = bot_directory(name) / "cron.yaml"
    if not path.exists():
        return {"jobs": []}
    with open(path) as file:
        data = yaml.safe_load(file)
    if not data or "jobs" not in data:
        return {"jobs": []}
    return data
save_cron_config function · python · L146-L151 (6 LOC)
src/cclaw/config.py
def save_cron_config(name: str, config: dict[str, Any]) -> None:
    """Save a bot's cron.yaml."""
    path = bot_directory(name) / "cron.yaml"
    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "w") as file:
        yaml.dump(config, file, default_flow_style=False, allow_unicode=True)
bot_exists function · python · L180-L185 (6 LOC)
src/cclaw/config.py
def bot_exists(name: str) -> bool:
    """Check if a bot with this name already exists."""
    config = load_config()
    if not config or "bots" not in config:
        return False
    return any(b["name"] == name for b in config["bots"])
resolve_job_timezone function · python · L23-L35 (13 LOC)
src/cclaw/cron.py
def resolve_job_timezone(job: dict[str, Any]) -> ZoneInfo | timezone:
    """Resolve the timezone for a cron job.

    Returns ZoneInfo for named timezones (e.g., 'Asia/Seoul'),
    or timezone.utc as default.
    """
    timezone_name = job.get("timezone")
    if timezone_name:
        try:
            return ZoneInfo(timezone_name)
        except (KeyError, ValueError):
            logger.warning("Invalid timezone '%s', falling back to UTC", timezone_name)
    return timezone.utc
load_cron_config function · python · L46-L55 (10 LOC)
src/cclaw/cron.py
def load_cron_config(bot_name: str) -> dict[str, Any]:
    """Load a bot's cron.yaml. Returns empty config if it doesn't exist."""
    path = cron_config_path(bot_name)
    if not path.exists():
        return {"jobs": []}
    with open(path) as file:
        data = yaml.safe_load(file)
    if not data or "jobs" not in data:
        return {"jobs": []}
    return data
save_cron_config function · python · L58-L63 (6 LOC)
src/cclaw/cron.py
def save_cron_config(bot_name: str, config: dict[str, Any]) -> None:
    """Save a bot's cron.yaml."""
    path = cron_config_path(bot_name)
    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "w") as file:
        yaml.dump(config, file, default_flow_style=False, allow_unicode=True)
get_cron_job function · python · L72-L77 (6 LOC)
src/cclaw/cron.py
def get_cron_job(bot_name: str, job_name: str) -> dict[str, Any] | None:
    """Get a specific cron job by name."""
    for job in list_cron_jobs(bot_name):
        if job.get("name") == job_name:
            return job
    return None
Repobility · code-quality intelligence platform · https://repobility.com
add_cron_job function · python · L80-L107 (28 LOC)
src/cclaw/cron.py
def add_cron_job(bot_name: str, job: dict[str, Any]) -> None:
    """Add a cron job to a bot's configuration.

    If the job has an 'at' field with a relative duration (e.g., '10m', '2h'),
    it is converted to an absolute ISO datetime at add time so that the
    scheduler can correctly detect when the time has passed.
    """
    config = load_cron_config(bot_name)
    existing_names = {j["name"] for j in config["jobs"]}
    if job["name"] in existing_names:
        raise ValueError(f"Job '{job['name']}' already exists")

    # Convert relative duration to absolute ISO datetime
    if "at" in job:
        at_value = job["at"]
        duration_match = re.match(r"^(\d+)([mhd])$", str(at_value).strip())
        if duration_match:
            at_time = parse_one_shot_time(at_value)
            if at_time:
                job["at"] = at_time.isoformat()
                logger.info(
                    "Converted relative 'at' value '%s' to absolute '%s'",
                    at_value,
  
remove_cron_job function · python · L110-L118 (9 LOC)
src/cclaw/cron.py
def remove_cron_job(bot_name: str, job_name: str) -> bool:
    """Remove a cron job by name. Returns True if found and removed."""
    config = load_cron_config(bot_name)
    original_count = len(config["jobs"])
    config["jobs"] = [j for j in config["jobs"] if j.get("name") != job_name]
    if len(config["jobs"]) == original_count:
        return False
    save_cron_config(bot_name, config)
    return True
enable_cron_job function · python · L121-L129 (9 LOC)
src/cclaw/cron.py
def enable_cron_job(bot_name: str, job_name: str) -> bool:
    """Enable a cron job. Returns True if found."""
    config = load_cron_config(bot_name)
    for job in config["jobs"]:
        if job.get("name") == job_name:
            job["enabled"] = True
            save_cron_config(bot_name, config)
            return True
    return False
disable_cron_job function · python · L132-L140 (9 LOC)
src/cclaw/cron.py
def disable_cron_job(bot_name: str, job_name: str) -> bool:
    """Disable a cron job. Returns True if found."""
    config = load_cron_config(bot_name)
    for job in config["jobs"]:
        if job.get("name") == job_name:
            job["enabled"] = False
            save_cron_config(bot_name, config)
            return True
    return False
parse_one_shot_time function · python · L151-L180 (30 LOC)
src/cclaw/cron.py
def parse_one_shot_time(at_value: str) -> datetime | None:
    """Parse a one-shot time value.

    Supports:
    - ISO 8601 datetime: "2026-02-20T15:00:00"
    - Duration shorthand: "30m", "2h", "1d"

    Returns a timezone-aware datetime or None if parsing fails.
    """
    # Try duration shorthand first
    duration_match = re.match(r"^(\d+)([mhd])$", at_value.strip())
    if duration_match:
        amount = int(duration_match.group(1))
        unit = duration_match.group(2)
        now = datetime.now(timezone.utc)
        if unit == "m":
            return now + timedelta(minutes=amount)
        elif unit == "h":
            return now + timedelta(hours=amount)
        elif unit == "d":
            return now + timedelta(days=amount)

    # Try ISO 8601 datetime
    try:
        parsed = datetime.fromisoformat(at_value)
        if parsed.tzinfo is None:
            parsed = parsed.replace(tzinfo=timezone.utc)
        return parsed
    except ValueError:
        return None
next_run_time function · python · L183-L200 (18 LOC)
src/cclaw/cron.py
def next_run_time(job: dict[str, Any]) -> datetime | None:
    """Calculate the next run time for a job.

    Returns None if the job has no valid schedule.
    The returned datetime is timezone-aware in the job's configured timezone.
    """
    if "at" in job:
        at_time = parse_one_shot_time(job["at"])
        return at_time

    schedule = job.get("schedule")
    if not schedule or not validate_cron_schedule(schedule):
        return None

    job_timezone = resolve_job_timezone(job)
    now = datetime.now(job_timezone)
    cron = croniter(schedule, now)
    return cron.get_next(datetime).replace(tzinfo=job_timezone)
cron_session_directory function · python · L206-L219 (14 LOC)
src/cclaw/cron.py
def cron_session_directory(bot_name: str, job_name: str) -> Path:
    """Return the cron session directory for a job, ensuring it exists."""
    directory = bot_directory(bot_name) / "cron_sessions" / job_name
    directory.mkdir(parents=True, exist_ok=True)

    # Copy bot's CLAUDE.md if not present
    bot_claude_md = bot_directory(bot_name) / "CLAUDE.md"
    session_claude_md = directory / "CLAUDE.md"
    if not session_claude_md.exists() and bot_claude_md.exists():
        import shutil

        shutil.copy2(bot_claude_md, session_claude_md)

    return directory
execute_cron_job function · python · L225-L316 (92 LOC)
src/cclaw/cron.py
async def execute_cron_job(
    bot_name: str,
    job: dict[str, Any],
    bot_config: dict[str, Any],
    send_message_callback: Any,
) -> None:
    """Execute a single cron job and send results via callback.

    Args:
        bot_name: Name of the bot.
        job: The cron job configuration dict.
        bot_config: The bot's configuration.
        send_message_callback: Async callable(user_id, text) to send messages.
    """
    from cclaw.claude_runner import run_claude
    from cclaw.config import DEFAULT_MODEL
    from cclaw.utils import markdown_to_telegram_html, split_message

    job_name = job["name"]
    raw_message = job.get("message", "")
    model = job.get("model") or bot_config.get("model", DEFAULT_MODEL)
    job_skills = job.get("skills") or bot_config.get("skills", [])
    command_timeout = bot_config.get("command_timeout", 300)

    working_directory = str(cron_session_directory(bot_name, job_name))

    from cclaw.session import load_bot_memory, load_global_memor
All rows above produced by Repobility · https://repobility.com
run_cron_scheduler function · python · L319-L414 (96 LOC)
src/cclaw/cron.py
async def run_cron_scheduler(
    bot_name: str,
    bot_config: dict[str, Any],
    application: Any,
    stop_event: asyncio.Event,
) -> None:
    """Run the cron scheduler loop for a bot.

    Checks every 30 seconds for jobs that need to run.
    Uses application.bot.send_message as the message callback.

    Args:
        bot_name: Name of the bot.
        bot_config: The bot's configuration.
        application: The telegram Application instance.
        stop_event: Event that signals shutdown.
    """
    last_run_times: dict[str, datetime] = {}

    logger.info("Cron scheduler started for bot '%s'", bot_name)

    while not stop_event.is_set():
        try:
            jobs = list_cron_jobs(bot_name)

            for job in jobs:
                job_name = job.get("name")
                if not job_name:
                    continue

                if not job.get("enabled", True):
                    continue

                should_run = False

                if "at" in job:
get_heartbeat_config function · python · L31-L36 (6 LOC)
src/cclaw/heartbeat.py
def get_heartbeat_config(bot_name: str) -> dict[str, Any]:
    """Read the heartbeat section from bot.yaml."""
    bot_config = load_bot_config(bot_name)
    if not bot_config:
        return dict(DEFAULT_HEARTBEAT_CONFIG)
    return bot_config.get("heartbeat", dict(DEFAULT_HEARTBEAT_CONFIG))
save_heartbeat_config function · python · L39-L45 (7 LOC)
src/cclaw/heartbeat.py
def save_heartbeat_config(bot_name: str, heartbeat_config: dict[str, Any]) -> None:
    """Save the heartbeat section to bot.yaml."""
    bot_config = load_bot_config(bot_name)
    if not bot_config:
        return
    bot_config["heartbeat"] = heartbeat_config
    save_bot_config(bot_name, bot_config)
enable_heartbeat function · python · L48-L67 (20 LOC)
src/cclaw/heartbeat.py
def enable_heartbeat(bot_name: str) -> bool:
    """Enable heartbeat for a bot.

    Creates default HEARTBEAT.md if missing. Returns True if successful.
    """
    bot_config = load_bot_config(bot_name)
    if not bot_config:
        return False
    heartbeat_config = bot_config.get("heartbeat", dict(DEFAULT_HEARTBEAT_CONFIG))
    heartbeat_config["enabled"] = True
    bot_config["heartbeat"] = heartbeat_config
    save_bot_config(bot_name, bot_config)

    # Create default HEARTBEAT.md if it doesn't exist
    session_directory = heartbeat_session_directory(bot_name)
    heartbeat_md_path = session_directory / "HEARTBEAT.md"
    if not heartbeat_md_path.exists():
        heartbeat_md_path.write_text(default_heartbeat_content())

    return True
disable_heartbeat function · python · L70-L79 (10 LOC)
src/cclaw/heartbeat.py
def disable_heartbeat(bot_name: str) -> bool:
    """Disable heartbeat for a bot. Returns True if successful."""
    bot_config = load_bot_config(bot_name)
    if not bot_config:
        return False
    heartbeat_config = bot_config.get("heartbeat", dict(DEFAULT_HEARTBEAT_CONFIG))
    heartbeat_config["enabled"] = False
    bot_config["heartbeat"] = heartbeat_config
    save_bot_config(bot_name, bot_config)
    return True
is_within_active_hours function · python · L85-L112 (28 LOC)
src/cclaw/heartbeat.py
def is_within_active_hours(active_hours: dict[str, str], now: datetime | None = None) -> bool:
    """Check if the current time is within the active hours range.

    Uses local time. Supports overnight ranges (e.g. start=22:00, end=06:00).

    Args:
        active_hours: Dict with 'start' and 'end' keys in HH:MM format.
        now: Optional datetime for testing. Uses local time if None.
    """
    if now is None:
        now = datetime.now()

    start_str = active_hours.get("start", "00:00")
    end_str = active_hours.get("end", "23:59")

    start_hour, start_minute = map(int, start_str.split(":"))
    end_hour, end_minute = map(int, end_str.split(":"))

    current_minutes = now.hour * 60 + now.minute
    start_minutes = start_hour * 60 + start_minute
    end_minutes = end_hour * 60 + end_minute

    if start_minutes <= end_minutes:
        # Normal range (e.g. 07:00 - 23:00)
        return start_minutes <= current_minutes <= end_minutes
    else:
        # Overnight range (e.g.
heartbeat_session_directory function · python · L115-L133 (19 LOC)
src/cclaw/heartbeat.py
def heartbeat_session_directory(bot_name: str) -> Path:
    """Return the heartbeat session directory, ensuring it exists.

    Creates the directory and copies bot's CLAUDE.md if not present.
    """
    directory = bot_directory(bot_name) / "heartbeat_sessions"
    directory.mkdir(parents=True, exist_ok=True)

    # Create workspace subdirectory
    workspace = directory / "workspace"
    workspace.mkdir(exist_ok=True)

    # Copy bot's CLAUDE.md if not present
    bot_claude_md = bot_directory(bot_name) / "CLAUDE.md"
    session_claude_md = directory / "CLAUDE.md"
    if not session_claude_md.exists() and bot_claude_md.exists():
        shutil.copy2(bot_claude_md, session_claude_md)

    return directory
default_heartbeat_content function · python · L139-L154 (16 LOC)
src/cclaw/heartbeat.py
def default_heartbeat_content() -> str:
    """Return the default HEARTBEAT.md template."""
    return """# Heartbeat Checklist

실행할 항목이 없으면 HEARTBEAT_OK만 출력하세요.

## 확인 항목

- workspace/에 미완성 작업이 있는지 확인
- 마지막 대화 이후 8시간 이상 지났으면 가벼운 안부

## 규칙

- 긴급하지 않으면 메시지 보내지 말 것
- HEARTBEAT_OK는 반드시 응답 마지막에 포함할 것
"""
Repobility — same analyzer, your code, free for public repos · /scan/
load_heartbeat_markdown function · python · L157-L163 (7 LOC)
src/cclaw/heartbeat.py
def load_heartbeat_markdown(bot_name: str) -> str:
    """Load the HEARTBEAT.md content for a bot."""
    directory = heartbeat_session_directory(bot_name)
    heartbeat_md_path = directory / "HEARTBEAT.md"
    if not heartbeat_md_path.exists():
        return ""
    return heartbeat_md_path.read_text()
execute_heartbeat function · python · L176-L284 (109 LOC)
src/cclaw/heartbeat.py
async def execute_heartbeat(
    bot_name: str,
    bot_config: dict[str, Any],
    send_message_callback: Callable,
) -> None:
    """Execute a heartbeat check and notify users if needed.

    1. Prepare heartbeat session directory
    2. Read HEARTBEAT.md and compose prompt
    3. Run Claude with the prompt
    4. If response contains HEARTBEAT_OK → log only, no message
    5. If response does NOT contain HEARTBEAT_OK → send to allowed_users

    Args:
        bot_name: Name of the bot.
        bot_config: The bot's configuration.
        send_message_callback: Async callable(chat_id, text, ...) to send messages.
    """
    from cclaw.claude_runner import run_claude
    from cclaw.config import DEFAULT_MODEL
    from cclaw.utils import markdown_to_telegram_html, split_message

    model = bot_config.get("model", DEFAULT_MODEL)
    attached_skills = bot_config.get("skills", [])
    command_timeout = bot_config.get("command_timeout", 300)

    working_directory = str(heartbeat_session
run_heartbeat_scheduler function · python · L290-L356 (67 LOC)
src/cclaw/heartbeat.py
async def run_heartbeat_scheduler(
    bot_name: str,
    bot_config: dict[str, Any],
    application: Any,
    stop_event: asyncio.Event,
) -> None:
    """Run the heartbeat scheduler loop for a bot.

    Repeats at interval_minutes intervals:
    1. Check stop_event → exit if set
    2. Check is_within_active_hours() → skip if outside range
    3. Call execute_heartbeat()
    4. Sleep for interval_minutes

    Args:
        bot_name: Name of the bot.
        bot_config: The bot's configuration.
        application: The telegram Application instance.
        stop_event: Event that signals shutdown.
    """
    heartbeat_config = bot_config.get("heartbeat", {})
    interval_minutes = heartbeat_config.get("interval_minutes", 30)
    active_hours = heartbeat_config.get("active_hours", {"start": "07:00", "end": "23:00"})
    interval_seconds = interval_minutes * 60

    logger.info(
        "Heartbeat scheduler started for bot '%s' (interval: %dm, active: %s-%s)",
        bot_name,
      
check_claude_code function · python · L39-L58 (20 LOC)
src/cclaw/onboarding.py
def check_claude_code() -> EnvironmentCheckResult:
    """Check if Claude Code CLI is installed."""
    path = shutil.which("claude")
    if not path:
        return EnvironmentCheckResult(
            name="Claude Code",
            available=False,
            version="",
            message="Claude Code is not installed.\n\n"
            "  Install:\n"
            "    npm install -g @anthropic-ai/claude-code\n\n"
            "  Then run again: cclaw init",
        )
    try:
        result = subprocess.run(["claude", "--version"], capture_output=True, text=True, timeout=10)
        version = result.stdout.strip() or result.stderr.strip()
    except (subprocess.TimeoutExpired, OSError):
        version = "unknown"

    return EnvironmentCheckResult(name="Claude Code", available=True, version=version, message="")
check_node function · python · L61-L77 (17 LOC)
src/cclaw/onboarding.py
def check_node() -> EnvironmentCheckResult:
    """Check if Node.js is installed."""
    path = shutil.which("node")
    if not path:
        return EnvironmentCheckResult(
            name="Node.js",
            available=False,
            version="",
            message="Node.js is not installed. Required for Claude Code.",
        )
    try:
        result = subprocess.run(["node", "--version"], capture_output=True, text=True, timeout=10)
        version = result.stdout.strip()
    except (subprocess.TimeoutExpired, OSError):
        version = "unknown"

    return EnvironmentCheckResult(name="Node.js", available=True, version=version, message="")
check_python function · python · L80-L85 (6 LOC)
src/cclaw/onboarding.py
def check_python() -> EnvironmentCheckResult:
    """Check Python version."""
    import sys

    version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
    return EnvironmentCheckResult(name="Python", available=True, version=version, message="")
display_environment_checks function · python · L93-L105 (13 LOC)
src/cclaw/onboarding.py
def display_environment_checks(checks: list[EnvironmentCheckResult]) -> bool:
    """Display environment check results. Returns True if all passed."""
    console.print("\nChecking environment...")
    all_passed = True
    for check in checks:
        if check.available:
            console.print(f"  [green]OK[/green] {check.name} {check.version}")
        else:
            console.print(f"  [red]FAIL[/red] {check.name}")
            if check.message:
                console.print(f"\n  {check.message}")
            all_passed = False
    return all_passed
validate_telegram_token function · python · L108-L120 (13 LOC)
src/cclaw/onboarding.py
async def validate_telegram_token(token: str) -> dict | None:
    """Validate a Telegram bot token. Returns bot info dict or None."""
    from telegram import Bot

    try:
        bot = Bot(token=token)
        bot_info = await bot.get_me()
        return {
            "username": f"@{bot_info.username}",
            "botname": bot_info.first_name,
        }
    except Exception:
        return None
Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
prompt_telegram_token function · python · L123-L152 (30 LOC)
src/cclaw/onboarding.py
def prompt_telegram_token() -> tuple[str, dict]:
    """Prompt user for Telegram bot token with retry. Returns (token, bot_info)."""
    from cclaw.utils import prompt_input

    console.print("\n[bold]Connecting Telegram bot.[/bold]")
    console.print()
    console.print("  1. Send a DM to @BotFather on Telegram.")
    console.print("  2. Create a bot with the /newbot command.")
    console.print("  3. Enter the issued token below.")
    console.print()

    for attempt in range(MAXIMUM_TOKEN_RETRY):
        token = prompt_input("Bot Token:")
        console.print("Verifying token...")

        bot_info = asyncio.run(validate_telegram_token(token))
        if bot_info:
            console.print(
                f"[green]OK[/green] Bot verified: {bot_info['username']} ({bot_info['botname']})"
            )
            return token, bot_info

        remaining = MAXIMUM_TOKEN_RETRY - attempt - 1
        if remaining > 0:
            console.print(f"[red]Invalid token. {remaining} attem
prompt_bot_profile function · python · L155-L179 (25 LOC)
src/cclaw/onboarding.py
def prompt_bot_profile() -> dict:
    """Prompt user for bot profile information. Returns profile dict."""
    from cclaw.utils import prompt_input, prompt_multiline

    console.print("\n[bold]Setting up bot profile.[/bold]\n")

    while True:
        name = prompt_input("Bot name (English, used as directory name):")
        name = name.strip().lower().replace(" ", "-")
        if not name.isascii() or not name.replace("-", "").isalnum():
            console.print("[red]Use only English letters, numbers, and hyphens.[/red]")
            continue
        if bot_exists(name):
            console.print(f"[red]Bot '{name}' already exists.[/red]")
            continue
        break

    personality = prompt_multiline("Bot personality:")
    description = prompt_multiline("Bot role/tasks:")

    return {
        "name": name,
        "personality": personality,
        "description": description,
    }
_restart_daemon function · python · L189-L199 (11 LOC)
src/cclaw/onboarding.py
def _restart_daemon() -> None:
    """Restart the cclaw daemon to pick up new bot."""
    from cclaw.bot_manager import start_bots, stop_bots

    console.print("\n[yellow]Restarting daemon to register new bot...[/yellow]")
    try:
        stop_bots()
        start_bots(daemon=True)
    except Exception as error:
        console.print(f"[red]Failed to restart daemon: {error}[/red]")
        console.print("  Restart manually: [bold]cclaw stop && cclaw start --daemon[/bold]")
create_bot function · python · L202-L243 (42 LOC)
src/cclaw/onboarding.py
def create_bot(token: str, bot_info: dict, profile: dict) -> None:
    """Create bot configuration files."""
    bot_config = {
        "telegram_token": token,
        "telegram_username": bot_info["username"],
        "telegram_botname": bot_info["botname"],
        "description": profile["description"],
        "personality": profile["personality"],
        "allowed_users": [],
        "claude_args": [],
        "streaming": False,
        "heartbeat": {
            "enabled": False,
            "interval_minutes": 30,
            "active_hours": {
                "start": "07:00",
                "end": "23:00",
            },
        },
    }

    save_bot_config(profile["name"], bot_config)
    add_bot_to_config(profile["name"])

    home = cclaw_home()
    console.print()
    console.print(
        Panel(
            f"[green]OK[/green] {profile['name']} created!\n\n"
            f"  Name:      {profile['name']}\n"
            f"  Personality: {profile['personality']}\n"
       
run_onboarding function · python · L246-L258 (13 LOC)
src/cclaw/onboarding.py
def run_onboarding() -> None:
    """Run the full onboarding flow."""
    console.print("[bold]Starting cclaw initial setup.[/bold]")

    checks = run_environment_checks()
    if not display_environment_checks(checks):
        raise typer.Exit(1)

    console.print("\n[green]Environment check passed![/green]")

    token, bot_info = prompt_telegram_token()
    profile = prompt_bot_profile()
    create_bot(token, bot_info, profile)
run_doctor function · python · L268-L311 (44 LOC)
src/cclaw/onboarding.py
def run_doctor() -> None:
    """Run environment and configuration diagnostics."""
    console.print("[bold]cclaw doctor[/bold]\n")

    checks = run_environment_checks()
    display_environment_checks(checks)

    console.print()

    config = load_config()
    if config is None:
        console.print("[yellow]No config.yaml found. Run 'cclaw init' first.[/yellow]")
        return

    console.print("[green]OK[/green] config.yaml found")
    console.print(f"  Log level: {config.get('settings', {}).get('log_level', 'N/A')}")

    bots = config.get("bots", [])
    if not bots:
        console.print("[yellow]No bots configured.[/yellow]")
        return

    console.print(f"\n[bold]Bots ({len(bots)}):[/bold]")

    for bot_entry in bots:
        name = bot_entry["name"]
        bot_config = load_bot_config(name)
        if not bot_config:
            console.print(f"  [red]FAIL[/red] {name}: bot.yaml missing")
            continue

        token = bot_config.get("telegram_token", "")
   
collect_session_chat_ids function · python · L25-L43 (19 LOC)
src/cclaw/session.py
def collect_session_chat_ids(bot_path: Path) -> list[int]:
    """Collect chat IDs from existing session directories.

    Scans ``sessions/chat_<id>/`` directories and returns the list of chat IDs.
    Used as a fallback when ``allowed_users`` is empty but the bot needs to
    send proactive messages (cron results, heartbeat notifications).
    """
    sessions_directory = bot_path / "sessions"
    if not sessions_directory.exists():
        return []
    chat_ids: list[int] = []
    for child in sorted(sessions_directory.iterdir()):
        if child.is_dir() and child.name.startswith("chat_"):
            try:
                chat_id = int(child.name.removeprefix("chat_"))
                chat_ids.append(chat_id)
            except ValueError:
                continue
    return chat_ids
ensure_session function · python · L46-L66 (21 LOC)
src/cclaw/session.py
def ensure_session(bot_path: Path, chat_id: int) -> Path:
    """Ensure a session directory exists with required files.

    Creates the session directory, copies the bot's CLAUDE.md into it,
    and creates the workspace subdirectory.

    Returns the session directory path.
    """
    directory = session_directory(bot_path, chat_id)
    directory.mkdir(parents=True, exist_ok=True)

    workspace = directory / "workspace"
    workspace.mkdir(exist_ok=True)

    session_claude_md = directory / "CLAUDE.md"
    bot_claude_md = bot_path / "CLAUDE.md"

    if not session_claude_md.exists() and bot_claude_md.exists():
        shutil.copy2(bot_claude_md, session_claude_md)

    return directory
Repobility · code-quality intelligence platform · https://repobility.com
_list_all_conversation_files function · python · L75-L83 (9 LOC)
src/cclaw/session.py
def _list_all_conversation_files(session_directory: Path) -> list[Path]:
    """List all conversation files (dated + legacy) in the session directory."""
    if not session_directory.exists():
        return []
    files = list(session_directory.glob(CONVERSATION_GLOB_PATTERN))
    legacy_file = session_directory / "conversation.md"
    if legacy_file.exists():
        files.append(legacy_file)
    return files
conversation_status_summary function · python · L86-L108 (23 LOC)
src/cclaw/session.py
def conversation_status_summary(session_directory: Path) -> str:
    """Return a human-readable summary of conversation files in the session."""
    if not session_directory.exists():
        return "No conversation yet"

    conversation_files = list(session_directory.glob(CONVERSATION_GLOB_PATTERN))
    legacy_file = session_directory / "conversation.md"

    total_size = 0
    file_count = 0

    for file in conversation_files:
        total_size += file.stat().st_size
        file_count += 1

    if legacy_file.exists():
        total_size += legacy_file.stat().st_size
        file_count += 1

    if file_count == 0:
        return "No conversation yet"

    return f"{total_size:,} bytes ({file_count} files)"
reset_session function · python · L111-L116 (6 LOC)
src/cclaw/session.py
def reset_session(bot_path: Path, chat_id: int) -> None:
    """Reset a session by deleting all conversation files (keep workspace)."""
    directory = session_directory(bot_path, chat_id)
    for conversation_file in _list_all_conversation_files(directory):
        conversation_file.unlink()
    clear_claude_session_id(directory)
‹ prevpage 2 / 3next ›