diff --git a/.claude/skills/claw/SKILL.md b/.claude/skills/claw/SKILL.md new file mode 100644 index 0000000..10e0dc3 --- /dev/null +++ b/.claude/skills/claw/SKILL.md @@ -0,0 +1,131 @@ +--- +name: claw +description: Install the claw CLI tool — run NanoClaw agent containers from the command line without opening a chat app. +--- + +# claw — NanoClaw CLI + +`claw` is a Python CLI that sends prompts directly to a NanoClaw agent container from the terminal. It reads registered groups from the NanoClaw database, picks up secrets from `.env`, and pipes a JSON payload into a container run — no chat app required. + +## What it does + +- Send a prompt to any registered group by name, folder, or JID +- Default target is the main group (no `-g` needed for most use) +- Resume a previous session with `-s ` +- Read prompts from stdin (`--pipe`) for scripting and piping +- List all registered groups with `--list-groups` +- Auto-detects `container` or `docker` runtime (or override with `--runtime`) +- Prints the agent's response to stdout; session ID to stderr +- Verbose mode (`-v`) shows the command, redacted payload, and exit code + +## Prerequisites + +- Python 3.8 or later +- NanoClaw installed with a built and tagged container image (`nanoclaw-agent:latest`) +- Either `container` (Apple Container, macOS 15+) or `docker` available in `PATH` + +## Install + +Run this skill from within the NanoClaw directory. The script auto-detects its location, so the symlink always points to the right place. + +### 1. Copy the script + +```bash +mkdir -p scripts +cp "${CLAUDE_SKILL_DIR}/scripts/claw" scripts/claw +chmod +x scripts/claw +``` + +### 2. Symlink into PATH + +```bash +mkdir -p ~/bin +ln -sf "$(pwd)/scripts/claw" ~/bin/claw +``` + +Make sure `~/bin` is in `PATH`. Add this to `~/.zshrc` or `~/.bashrc` if needed: + +```bash +export PATH="$HOME/bin:$PATH" +``` + +Then reload the shell: + +```bash +source ~/.zshrc # or ~/.bashrc +``` + +### 3. Verify + +```bash +claw --list-groups +``` + +You should see registered groups. If NanoClaw isn't running or the database doesn't exist yet, the list will be empty — that's fine. + +## Usage Examples + +```bash +# Send a prompt to the main group +claw "What's on my calendar today?" + +# Send to a specific group by name (fuzzy match) +claw -g "family" "Remind everyone about dinner at 7" + +# Send to a group by exact JID +claw -j "120363336345536173@g.us" "Hello" + +# Resume a previous session +claw -s abc123 "Continue where we left off" + +# Read prompt from stdin +echo "Summarize this" | claw --pipe -g dev + +# Pipe a file +cat report.txt | claw --pipe "Summarize this report" + +# List all registered groups +claw --list-groups + +# Force a specific runtime +claw --runtime docker "Hello" + +# Use a custom image tag (e.g. after rebuilding with a new tag) +claw --image nanoclaw-agent:dev "Hello" + +# Verbose mode (debug info, secrets redacted) +claw -v "Hello" + +# Custom timeout for long-running tasks +claw --timeout 600 "Run the full analysis" +``` + +## Troubleshooting + +### "neither 'container' nor 'docker' found" + +Install Docker Desktop or Apple Container (macOS 15+), or pass `--runtime` explicitly. + +### "no secrets found in .env" + +The script auto-detects your NanoClaw directory and reads `.env` from it. Check that the file exists and contains at least one of: `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`. + +### Container times out + +The default timeout is 300 seconds. For longer tasks, pass `--timeout 600` (or higher). If the container consistently hangs, check that your `nanoclaw-agent:latest` image is up to date by running `./container/build.sh`. + +### "group not found" + +Run `claw --list-groups` to see what's registered. Group lookup does a fuzzy partial match on name and folder — if your query matches multiple groups, you'll get an error listing the ambiguous matches. + +### Container crashes mid-stream + +Containers run with `--rm` so they are automatically removed. If the agent crashes before emitting the output sentinel, `claw` falls back to printing raw stdout. Use `-v` to see what the container produced. Rebuild the image with `./container/build.sh` if crashes are consistent. + +### Override the NanoClaw directory + +If `claw` can't find your database or `.env`, set the `NANOCLAW_DIR` environment variable: + +```bash +export NANOCLAW_DIR=/path/to/your/nanoclaw +``` diff --git a/.claude/skills/claw/scripts/claw b/.claude/skills/claw/scripts/claw new file mode 100644 index 0000000..3878e48 --- /dev/null +++ b/.claude/skills/claw/scripts/claw @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +""" +claw — NanoClaw CLI +Run a NanoClaw agent container from the command line. + +Usage: + claw "What is 2+2?" + claw -g "Review this code" + claw -g "" "What's the latest issue?" + claw -j "" "Hello" + claw -g -s "Continue" + claw --list-groups + echo "prompt text" | claw --pipe -g + cat prompt.txt | claw --pipe +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sqlite3 +import subprocess +import sys +import threading +from pathlib import Path + +# ── Globals ───────────────────────────────────────────────────────────────── + +VERBOSE = False + +def dbg(*args): + if VERBOSE: + print("»", *args, file=sys.stderr) + +# ── Config ────────────────────────────────────────────────────────────────── + +def _find_nanoclaw_dir() -> Path: + """Locate the NanoClaw installation directory. + + Resolution order: + 1. NANOCLAW_DIR env var + 2. The directory containing this script (if it looks like a NanoClaw install) + 3. ~/src/nanoclaw (legacy default) + """ + if env := os.environ.get("NANOCLAW_DIR"): + return Path(env).expanduser() + # If this script lives inside the NanoClaw tree (e.g. scripts/claw), walk up + here = Path(__file__).resolve() + for parent in [here.parent, here.parent.parent]: + if (parent / "store" / "messages.db").exists() or (parent / ".env").exists(): + return parent + return Path.home() / "src" / "nanoclaw" + +NANOCLAW_DIR = _find_nanoclaw_dir() +DB_PATH = NANOCLAW_DIR / "store" / "messages.db" +ENV_FILE = NANOCLAW_DIR / ".env" +IMAGE = "nanoclaw-agent:latest" + +SECRET_KEYS = [ + "CLAUDE_CODE_OAUTH_TOKEN", + "ANTHROPIC_API_KEY", + "ANTHROPIC_BASE_URL", + "ANTHROPIC_AUTH_TOKEN", + "OLLAMA_HOST", +] + +# ── Helpers ────────────────────────────────────────────────────────────────── + +def detect_runtime(preference: str | None) -> str: + if preference: + dbg(f"runtime: forced to {preference}") + return preference + for rt in ("container", "docker"): + result = subprocess.run(["which", rt], capture_output=True) + if result.returncode == 0: + dbg(f"runtime: auto-detected {rt} at {result.stdout.decode().strip()}") + return rt + sys.exit("error: neither 'container' nor 'docker' found. Install one or pass --runtime.") + + +def read_secrets(env_file: Path) -> dict: + secrets = {} + if not env_file.exists(): + return secrets + for line in env_file.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + if "=" in line: + key, _, val = line.partition("=") + key = key.strip() + if key in SECRET_KEYS: + secrets[key] = val.strip() + return secrets + + +def get_groups(db: Path) -> list[dict]: + conn = sqlite3.connect(db) + rows = conn.execute( + "SELECT jid, name, folder, is_main FROM registered_groups ORDER BY name" + ).fetchall() + conn.close() + return [{"jid": r[0], "name": r[1], "folder": r[2], "is_main": bool(r[3])} for r in rows] + + +def find_group(groups: list[dict], query: str) -> dict | None: + q = query.lower() + # Exact name match + for g in groups: + if g["name"].lower() == q or g["folder"].lower() == q: + return g + # Partial match + matches = [g for g in groups if q in g["name"].lower() or q in g["folder"].lower()] + if len(matches) == 1: + return matches[0] + if len(matches) > 1: + names = ", ".join(f'"{g["name"]}"' for g in matches) + sys.exit(f"error: ambiguous group '{query}'. Matches: {names}") + return None + + +def run_container(runtime: str, image: str, payload: dict, timeout: int = 300) -> None: + cmd = [runtime, "run", "-i", "--rm", image] + dbg(f"cmd: {' '.join(cmd)}") + + # Show payload sans secrets + if VERBOSE: + safe = {k: v for k, v in payload.items() if k != "secrets"} + safe["secrets"] = {k: "***" for k in payload.get("secrets", {})} + dbg(f"payload: {json.dumps(safe, indent=2)}") + + proc = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + dbg(f"container pid: {proc.pid}") + + # Write JSON payload and close stdin + proc.stdin.write(json.dumps(payload).encode()) + proc.stdin.close() + dbg("stdin closed, waiting for response...") + + stdout_lines: list[str] = [] + stderr_lines: list[str] = [] + done = threading.Event() + + def stream_stderr(): + for raw in proc.stderr: + line = raw.decode(errors="replace").rstrip() + if line.startswith("npm notice"): + continue + stderr_lines.append(line) + print(line, file=sys.stderr) + + def stream_stdout(): + for raw in proc.stdout: + line = raw.decode(errors="replace").rstrip() + stdout_lines.append(line) + dbg(f"stdout: {line}") + # Kill the container as soon as we see the closing sentinel — + # the Node.js event loop often keeps the process alive indefinitely. + if line.strip() == "---NANOCLAW_OUTPUT_END---": + dbg("output sentinel found, terminating container") + done.set() + try: + proc.kill() + except ProcessLookupError: + pass + return + + t_err = threading.Thread(target=stream_stderr, daemon=True) + t_out = threading.Thread(target=stream_stdout, daemon=True) + t_err.start() + t_out.start() + + # Wait for sentinel or timeout + if not done.wait(timeout=timeout): + # Also check if process exited naturally + t_out.join(timeout=2) + if not done.is_set(): + proc.kill() + sys.exit(f"error: container timed out after {timeout}s (no output sentinel received)") + + t_err.join(timeout=5) + t_out.join(timeout=5) + proc.wait() + dbg(f"container done (rc={proc.returncode}), {len(stdout_lines)} stdout lines") + stdout = "\n".join(stdout_lines) + + # Parse output block + match = re.search( + r"---NANOCLAW_OUTPUT_START---\n(.*?)\n---NANOCLAW_OUTPUT_END---", + stdout, + re.DOTALL, + ) + if match: + try: + data = json.loads(match.group(1)) + status = data.get("status", "unknown") + if status == "success": + print(data.get("result", "")) + session_id = data.get("newSessionId") or data.get("sessionId") + if session_id: + print(f"\n[session: {session_id}]", file=sys.stderr) + else: + print(f"[{status}] {data.get('result', '')}", file=sys.stderr) + sys.exit(1) + except json.JSONDecodeError: + print(match.group(1)) + else: + # No structured output — print raw stdout + print(stdout) + + if proc.returncode not in (0, None): + sys.exit(proc.returncode) + + +# ── Main ───────────────────────────────────────────────────────────────────── + +def main(): + parser = argparse.ArgumentParser( + prog="claw", + description="Run a NanoClaw agent from the command line.", + ) + parser.add_argument("prompt", nargs="?", help="Prompt to send") + parser.add_argument("-g", "--group", help="Group name or folder (fuzzy match)") + parser.add_argument("-j", "--jid", help="Chat JID (exact)") + parser.add_argument("-s", "--session", help="Session ID to resume") + parser.add_argument("-p", "--pipe", action="store_true", + help="Read prompt from stdin (can be combined with a prompt arg as prefix)") + parser.add_argument("--runtime", choices=["docker", "container"], + help="Container runtime (default: auto-detect)") + parser.add_argument("--image", default=IMAGE, help=f"Container image (default: {IMAGE})") + parser.add_argument("--list-groups", action="store_true", help="List registered groups and exit") + parser.add_argument("--raw", action="store_true", help="Print raw JSON output") + parser.add_argument("--timeout", type=int, default=300, metavar="SECS", + help="Max seconds to wait for a response (default: 300)") + parser.add_argument("-v", "--verbose", action="store_true", + help="Show debug info: cmd, payload (secrets redacted), stdout lines, exit code") + args = parser.parse_args() + + global VERBOSE + VERBOSE = args.verbose + + groups = get_groups(DB_PATH) if DB_PATH.exists() else [] + + if args.list_groups: + print(f"{'NAME':<35} {'FOLDER':<30} {'JID'}") + print("-" * 100) + for g in groups: + main_tag = " [main]" if g["is_main"] else "" + print(f"{g['name']:<35} {g['folder']:<30} {g['jid']}{main_tag}") + return + + # Resolve prompt: --pipe reads stdin, optionally prepended with positional arg + if args.pipe or (not sys.stdin.isatty() and not args.prompt): + stdin_text = sys.stdin.read().strip() + if args.prompt: + prompt = f"{args.prompt}\n\n{stdin_text}" + else: + prompt = stdin_text + else: + prompt = args.prompt + + if not prompt: + parser.print_help() + sys.exit(1) + + # Resolve group → jid + jid = args.jid + group_name = None + is_main = False + + if args.group: + g = find_group(groups, args.group) + if g is None: + sys.exit(f"error: group '{args.group}' not found. Run --list-groups to see options.") + jid = g["jid"] + group_name = g["name"] + is_main = g["is_main"] + elif not jid: + # Default: main group + mains = [g for g in groups if g["is_main"]] + if mains: + jid = mains[0]["jid"] + group_name = mains[0]["name"] + is_main = True + else: + sys.exit("error: no group specified and no main group found. Use -g or -j.") + + runtime = detect_runtime(args.runtime) + secrets = read_secrets(ENV_FILE) + + if not secrets: + print("warning: no secrets found in .env — agent may not be authenticated", file=sys.stderr) + + payload: dict = { + "prompt": prompt, + "chatJid": jid, + "isMain": is_main, + "secrets": secrets, + } + if group_name: + payload["groupFolder"] = group_name + if args.session: + payload["sessionId"] = args.session + payload["resumeAt"] = "latest" + + print(f"[{group_name or jid}] running via {runtime}...", file=sys.stderr) + run_container(runtime, args.image, payload, timeout=args.timeout) + + +if __name__ == "__main__": + main() diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8d33f7b..49fe366 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,14 +1,18 @@ + ## Type of Change -- [ ] **Skill** - adds a new skill in `.claude/skills/` +- [ ] **Feature skill** - adds a channel or integration (source code changes + SKILL.md) +- [ ] **Utility skill** - adds a standalone tool (code files in `.claude/skills//`, no source changes) +- [ ] **Operational/container skill** - adds a workflow or agent skill (SKILL.md only, no source changes) - [ ] **Fix** - bug fix or security fix to source code - [ ] **Simplification** - reduces or simplifies source code +- [ ] **Documentation** - docs, README, or CONTRIBUTING changes only ## Description ## For Skills -- [ ] I have not made any changes to source code -- [ ] My skill contains instructions for Claude to follow (not pre-built code) +- [ ] SKILL.md contains instructions, not inline code (code goes in separate files) +- [ ] SKILL.md is under 500 lines - [ ] I tested this skill on a fresh clone diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml new file mode 100644 index 0000000..bec9d3e --- /dev/null +++ b/.github/workflows/label-pr.yml @@ -0,0 +1,35 @@ +name: Label PR + +on: + pull_request: + types: [opened, edited] + +jobs: + label: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/github-script@v7 + with: + script: | + const body = context.payload.pull_request.body || ''; + const labels = []; + + if (body.includes('[x] **Feature skill**')) { labels.push('PR: Skill'); labels.push('PR: Feature'); } + else if (body.includes('[x] **Utility skill**')) labels.push('PR: Skill'); + else if (body.includes('[x] **Operational/container skill**')) labels.push('PR: Skill'); + else if (body.includes('[x] **Fix**')) labels.push('PR: Fix'); + else if (body.includes('[x] **Simplification**')) labels.push('PR: Refactor'); + else if (body.includes('[x] **Documentation**')) labels.push('PR: Docs'); + + if (body.includes('contributing-guide: v1')) labels.push('follows-guidelines'); + + if (labels.length > 0) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels, + }); + } diff --git a/CLAUDE.md b/CLAUDE.md index 318d6dd..6351ff4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,10 +19,17 @@ Single Node.js process with skill-based channel system. Channels (WhatsApp, Tele | `src/task-scheduler.ts` | Runs scheduled tasks | | `src/db.ts` | SQLite operations | | `groups/{name}/CLAUDE.md` | Per-group memory (isolated) | -| `container/skills/agent-browser.md` | Browser automation tool (available to all agents via Bash) | +| `container/skills/` | Skills loaded inside agent containers (browser, status, formatting) | ## Skills +Four types of skills exist in NanoClaw. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full taxonomy and guidelines. + +- **Feature skills** — merge a `skill/*` branch to add capabilities (e.g. `/add-telegram`, `/add-slack`) +- **Utility skills** — ship code files alongside SKILL.md (e.g. `/claw`) +- **Operational skills** — instruction-only workflows, always on `main` (e.g. `/setup`, `/debug`) +- **Container skills** — loaded inside agent containers at runtime (`container/skills/`) + | Skill | When to Use | |-------|-------------| | `/setup` | First-time installation, authentication, service configuration | @@ -32,6 +39,10 @@ Single Node.js process with skill-based channel system. Channels (WhatsApp, Tele | `/qodo-pr-resolver` | Fetch and fix Qodo PR review issues interactively or in batch | | `/get-qodo-rules` | Load org- and repo-level coding rules from Qodo before code tasks | +## Contributing + +Before creating a PR, adding a skill, or preparing any contribution, you MUST read [CONTRIBUTING.md](CONTRIBUTING.md). It covers accepted change types, the four skill types and their guidelines, SKILL.md format rules, PR requirements, and the pre-submission checklist (searching for existing PRs/issues, testing, description format). + ## Development Run commands directly—don't tell the user to run them. @@ -57,7 +68,7 @@ systemctl --user restart nanoclaw ## Troubleshooting -**WhatsApp not connecting after upgrade:** WhatsApp is now a separate channel fork, not bundled in core. Run `/add-whatsapp` (or `git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git && git fetch whatsapp main && (git merge whatsapp/main || { git checkout --theirs package-lock.json && git add package-lock.json && git merge --continue; }) && npm run build`) to install it. Existing auth credentials and groups are preserved. +**WhatsApp not connecting after upgrade:** WhatsApp is now a separate skill, not bundled in core. Run `/add-whatsapp` (or `npx tsx scripts/apply-skill.ts .claude/skills/add-whatsapp && npm run build`) to install it. Existing auth credentials and groups are preserved. ## Container Build Cache diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd3614d..7a7816a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,18 @@ # Contributing +## Before You Start + +1. **Check for existing work.** Search open PRs and issues before starting: + ```bash + gh pr list --repo qwibitai/nanoclaw --search "" + gh issue list --repo qwibitai/nanoclaw --search "" + ``` + If a related PR or issue exists, build on it rather than duplicating effort. + +2. **Check alignment.** Read the [Philosophy section in README.md](README.md#philosophy). Source code changes should only be things 90%+ of users need. Skills can be more niche, but should still be useful beyond a single person's setup. + +3. **One thing per PR.** Each PR should do one thing — one bug fix, one skill, one simplification. Don't mix unrelated changes in a single PR. + ## Source Code Changes **Accepted:** Bug fixes, security fixes, simplifications, reducing code. @@ -8,16 +21,127 @@ ## Skills -A [skill](https://code.claude.com/docs/en/skills) is a markdown file in `.claude/skills/` that teaches Claude Code how to transform a NanoClaw installation. +NanoClaw uses [Claude Code skills](https://code.claude.com/docs/en/skills) — markdown files with optional supporting files that teach Claude how to do something. There are four types of skills in NanoClaw, each serving a different purpose. -A PR that contributes a skill should not modify any source files. - -Your skill should contain the **instructions** Claude follows to add the feature—not pre-built code. See `/add-telegram` for a good example. - -### Why? +### Why skills? Every user should have clean and minimal code that does exactly what they need. Skills let users selectively add features to their fork without inheriting code for features they don't want. -### Testing +### Skill types -Test your skill by running it on a fresh clone before submitting. +#### 1. Feature skills (branch-based) + +Add capabilities to NanoClaw by merging a git branch. The SKILL.md contains setup instructions; the actual code lives on a `skill/*` branch. + +**Location:** `.claude/skills/` on `main` (instructions only), code on `skill/*` branch + +**Examples:** `/add-telegram`, `/add-slack`, `/add-discord`, `/add-gmail` + +**How they work:** +1. User runs `/add-telegram` +2. Claude follows the SKILL.md: fetches and merges the `skill/telegram` branch +3. Claude walks through interactive setup (env vars, bot creation, etc.) + +**Contributing a feature skill:** +1. Fork `qwibitai/nanoclaw` and branch from `main` +2. Make the code changes (new files, modified source, updated `package.json`, etc.) +3. Add a SKILL.md in `.claude/skills//` with setup instructions — step 1 should be merging the branch +4. Open a PR. We'll create the `skill/` branch from your work + +See `/add-telegram` for a good example. See [docs/skills-as-branches.md](docs/skills-as-branches.md) for the full system design. + +#### 2. Utility skills (with code files) + +Standalone tools that ship code files alongside the SKILL.md. The SKILL.md tells Claude how to install the tool; the code lives in the skill directory itself (e.g. in a `scripts/` subfolder). + +**Location:** `.claude/skills//` with supporting files + +**Examples:** `/claw` (Python CLI in `scripts/claw`) + +**Key difference from feature skills:** No branch merge needed. The code is self-contained in the skill directory and gets copied into place during installation. + +**Guidelines:** +- Put code in separate files, not inline in the SKILL.md +- Use `${CLAUDE_SKILL_DIR}` to reference files in the skill directory +- SKILL.md contains installation instructions, usage docs, and troubleshooting + +#### 3. Operational skills (instruction-only) + +Workflows and guides with no code changes. The SKILL.md is the entire skill — Claude follows the instructions to perform a task. + +**Location:** `.claude/skills/` on `main` + +**Examples:** `/setup`, `/debug`, `/customize`, `/update-nanoclaw`, `/update-skills` + +**Guidelines:** +- Pure instructions — no code files, no branch merges +- Use `AskUserQuestion` for interactive prompts +- These stay on `main` and are always available to every user + +#### 4. Container skills (agent runtime) + +Skills that run inside the agent container, not on the host. These teach the container agent how to use tools, format output, or perform tasks. They are synced into each group's `.claude/skills/` directory when a container starts. + +**Location:** `container/skills//` + +**Examples:** `agent-browser` (web browsing), `capabilities` (/capabilities command), `status` (/status command), `slack-formatting` (Slack mrkdwn syntax) + +**Key difference:** These are NOT invoked by the user on the host. They're loaded by Claude Code inside the container and influence how the agent behaves. + +**Guidelines:** +- Follow the same SKILL.md + frontmatter format +- Use `allowed-tools` frontmatter to scope tool permissions +- Keep them focused — the agent's context window is shared across all container skills + +### SKILL.md format + +All skills use the [Claude Code skills standard](https://code.claude.com/docs/en/skills): + +```markdown +--- +name: my-skill +description: What this skill does and when to use it. +--- + +Instructions here... +``` + +**Rules:** +- Keep SKILL.md **under 500 lines** — move detail to separate reference files +- `name`: lowercase, alphanumeric + hyphens, max 64 chars +- `description`: required — Claude uses this to decide when to invoke the skill +- Put code in separate files, not inline in the markdown +- See the [skills standard](https://code.claude.com/docs/en/skills) for all available frontmatter fields + +## Testing + +Test your contribution on a fresh clone before submitting. For skills, run the skill end-to-end and verify it works. + +## Pull Requests + +### Before opening + +1. **Link related issues.** If your PR resolves an open issue, include `Closes #123` in the description so it's auto-closed on merge. +2. **Test thoroughly.** Run the feature yourself. For skills, test on a fresh clone. +3. **Check the right box** in the PR template. Labels are auto-applied based on your selection: + +| Checkbox | Label | +|----------|-------| +| Feature skill | `PR: Skill` + `PR: Feature` | +| Utility skill | `PR: Skill` | +| Operational/container skill | `PR: Skill` | +| Fix | `PR: Fix` | +| Simplification | `PR: Refactor` | +| Documentation | `PR: Docs` | + +### PR description + +Keep it concise. Remove any template sections that don't apply. The description should cover: + +- **What** — what the PR adds or changes +- **Why** — the motivation +- **How it works** — brief explanation of the approach +- **How it was tested** — what you did to verify it works +- **Usage** — how the user invokes it (for skills) + +Don't pad the description. A few clear sentences are better than lengthy paragraphs. diff --git a/README.md b/README.md index d76d33b..3aafd85 100644 --- a/README.md +++ b/README.md @@ -16,25 +16,6 @@ --- -

🐳 Now Runs in Docker Sandboxes

-

Every agent gets its own isolated container inside a micro VM.
Hypervisor-level isolation. Millisecond startup. No complex setup.

- -**macOS (Apple Silicon)** -```bash -curl -fsSL https://nanoclaw.dev/install-docker-sandboxes.sh | bash -``` - -**Windows (WSL)** -```bash -curl -fsSL https://nanoclaw.dev/install-docker-sandboxes-windows.sh | bash -``` - -> Currently supported on macOS (Apple Silicon) and Windows (x86). Linux support coming soon. - -

Read the announcement →  ·  Manual setup guide →

- ---- - ## Why I Built NanoClaw [OpenClaw](https://github.com/openclaw/openclaw) is an impressive project, but I wouldn't have been able to sleep if I had given complex software I didn't understand full access to my life. OpenClaw has nearly half a million lines of code, 53 config files, and 70+ dependencies. Its security is at the application level (allowlists, pairing codes) rather than true OS-level isolation. Everything runs in one Node process with shared memory. @@ -89,7 +70,7 @@ Then run `/setup`. Claude Code handles everything: dependencies, authentication, - **Main channel** - Your private channel (self-chat) for admin control; every group is completely isolated - **Scheduled tasks** - Recurring jobs that run Claude and can message you back - **Web access** - Search and fetch content from the Web -- **Container isolation** - Agents are sandboxed in [Docker Sandboxes](https://nanoclaw.dev/blog/nanoclaw-docker-sandboxes) (micro VM isolation), Apple Container (macOS), or Docker (macOS/Linux) +- **Container isolation** - Agents are sandboxed in Docker (macOS/Linux), [Docker Sandboxes](docs/docker-sandboxes.md) (micro VM isolation), or Apple Container (macOS) - **Agent Swarms** - Spin up teams of specialized agents that collaborate on complex tasks - **Optional integrations** - Add Gmail (`/add-gmail`) and more via skills @@ -138,9 +119,6 @@ Skills we'd like to see: **Communication Channels** - `/add-signal` - Add Signal as a channel -**Session Management** -- `/clear` - Add a `/clear` command that compacts the conversation (summarizes context while preserving critical information in the same session). Requires figuring out how to trigger compaction programmatically via the Claude Agent SDK. - ## Requirements - macOS or Linux @@ -173,7 +151,7 @@ Key files: **Why Docker?** -Docker provides cross-platform support (macOS, Linux and even Windows via WSL2) and a mature ecosystem. On macOS, you can optionally switch to Apple Container via `/convert-to-apple-container` for a lighter-weight native runtime. +Docker provides cross-platform support (macOS, Linux and even Windows via WSL2) and a mature ecosystem. On macOS, you can optionally switch to Apple Container via `/convert-to-apple-container` for a lighter-weight native runtime. For additional isolation, [Docker Sandboxes](docs/docker-sandboxes.md) run each container inside a micro VM. **Can I run this on Linux?** diff --git a/container/skills/slack-formatting/SKILL.md b/container/skills/slack-formatting/SKILL.md new file mode 100644 index 0000000..29a1b87 --- /dev/null +++ b/container/skills/slack-formatting/SKILL.md @@ -0,0 +1,94 @@ +--- +name: slack-formatting +description: Format messages for Slack using mrkdwn syntax. Use when responding to Slack channels (folder starts with "slack_" or JID contains slack identifiers). +--- + +# Slack Message Formatting (mrkdwn) + +When responding to Slack channels, use Slack's mrkdwn syntax instead of standard Markdown. + +## How to detect Slack context + +Check your group folder name or workspace path: +- Folder starts with `slack_` (e.g., `slack_engineering`, `slack_general`) +- Or check `/workspace/group/` path for `slack_` prefix + +## Formatting reference + +### Text styles + +| Style | Syntax | Example | +|-------|--------|---------| +| Bold | `*text*` | *bold text* | +| Italic | `_text_` | _italic text_ | +| Strikethrough | `~text~` | ~strikethrough~ | +| Code (inline) | `` `code` `` | `inline code` | +| Code block | ` ```code``` ` | Multi-line code | + +### Links and mentions + +``` + # Named link + # Auto-linked URL +<@U1234567890> # Mention user by ID +<#C1234567890> # Mention channel by ID + # @here + # @channel +``` + +### Lists + +Slack supports simple bullet lists but NOT numbered lists: + +``` +• First item +• Second item +• Third item +``` + +Use `•` (bullet character) or `- ` or `* ` for bullets. + +### Block quotes + +``` +> This is a block quote +> It can span multiple lines +``` + +### Emoji + +Use standard emoji shortcodes: `:white_check_mark:`, `:x:`, `:rocket:`, `:tada:` + +## What NOT to use + +- **NO** `##` headings (use `*Bold text*` for headers instead) +- **NO** `**double asterisks**` for bold (use `*single asterisks*`) +- **NO** `[text](url)` links (use `` instead) +- **NO** `1.` numbered lists (use bullets with numbers: `• 1. First`) +- **NO** tables (use code blocks or plain text alignment) +- **NO** `---` horizontal rules + +## Example message + +``` +*Daily Standup Summary* + +_March 21, 2026_ + +• *Completed:* Fixed authentication bug in login flow +• *In Progress:* Building new dashboard widgets +• *Blocked:* Waiting on API access from DevOps + +> Next sync: Monday 10am + +:white_check_mark: All tests passing | +``` + +## Quick rules + +1. Use `*bold*` not `**bold**` +2. Use `` not `[text](url)` +3. Use `•` bullets, avoid numbered lists +4. Use `:emoji:` shortcodes +5. Quote blocks with `>` +6. Skip headings — use bold text instead diff --git a/docs/nanoclaw-architecture-final.md b/docs/nanoclaw-architecture-final.md deleted file mode 100644 index 103b38b..0000000 --- a/docs/nanoclaw-architecture-final.md +++ /dev/null @@ -1,1063 +0,0 @@ -# NanoClaw Skills Architecture - -## Core Principle - -Skills are self-contained, auditable packages that apply programmatically via standard git merge mechanics. Claude Code orchestrates the process — running git commands, reading skill manifests, and stepping in only when git can't resolve a conflict on its own. The system uses existing git features (`merge-file`, `rerere`, `apply`) rather than custom merge infrastructure. - -### The Three-Level Resolution Model - -Every operation in the system follows this escalation: - -1. **Git** — deterministic, programmatic. `git merge-file` merges, `git rerere` replays cached resolutions, structured operations apply without merging. No AI involved. This handles the vast majority of cases. -2. **Claude Code** — reads `SKILL.md`, `.intent.md`, migration guides, and `state.yaml` to understand context. Resolves conflicts that git can't handle programmatically. Caches the resolution via `git rerere` so it never needs to resolve the same conflict again. -3. **User** — Claude Code asks the user when it lacks context or intent. This happens when two features genuinely conflict at an application level (not just a text-level merge conflict) and a human decision is needed about desired behavior. - -The goal is that Level 1 handles everything on a mature, well-tested installation. Level 2 handles first-time conflicts and edge cases. Level 3 is rare and only for genuine ambiguity. - -**Important**: a clean merge (exit code 0) does not guarantee working code. Semantic conflicts — a renamed variable, a shifted reference, a changed function signature — can produce clean text merges that break at runtime. **Tests must run after every operation**, regardless of whether the merge was clean. A clean merge with failing tests escalates to Level 2. - -### Safe Operations via Backup/Restore - -Many users clone the repo without forking, don't commit their changes, and don't think of themselves as git users. The system must work safely for them without requiring any git knowledge. - -Before any operation, the system copies all files that will be modified to `.nanoclaw/backup/`. On success, the backup is deleted. On failure, the backup is restored. This provides rollback safety regardless of whether the user commits, pushes, or understands git. - ---- - -## 1. The Shared Base - -`.nanoclaw/base/` holds the clean core — the original codebase before any skills or customizations were applied. This is the stable common ancestor for all three-way merges, and it only changes on core updates. - -- `git merge-file` uses the base to compute two diffs: what the user changed (current vs base) and what the skill wants to change (base vs skill's modified file), then combines both -- The base enables drift detection: if a file's hash differs from its base hash, something has been modified (skills, user customizations, or both) -- Each skill's `modify/` files contain the full file as it should look with that skill applied (including any prerequisite skill changes), all authored against the same clean core base - -On a **fresh codebase**, the user's files are identical to the base. This means `git merge-file` always exits cleanly for the first skill — the merge trivially produces the skill's modified version. No special-casing needed. - -When multiple skills modify the same file, the three-way merge handles the overlap naturally. If Telegram and Discord both modify `src/index.ts`, and both skill files include the Telegram changes, those common changes merge cleanly against the base. The result is the base + all skill changes + user customizations. - ---- - -## 2. Two Types of Changes: Code Merges vs. Structured Operations - -Not all files should be merged as text. The system distinguishes between **code files** (merged via `git merge-file`) and **structured data** (modified via deterministic operations). - -### Code Files (Three-Way Merge) - -Source code files where skills weave in logic — route handlers, middleware, business logic. These are merged using `git merge-file` against the shared base. The skill carries a full modified version of the file. - -### Structured Data (Deterministic Operations) - -Files like `package.json`, `docker-compose.yml`, `.env.example`, and generated configs are not code you merge — they're structured data you aggregate. Multiple skills adding npm dependencies to `package.json` shouldn't require a three-way text merge. Instead, skills declare their structured requirements in the manifest, and the system applies them programmatically. - -**Structured operations are implicit.** If a skill declares `npm_dependencies`, the system handles dependency installation automatically. There is no need for the skill author to add `npm install` to `post_apply`. When multiple skills are applied in sequence, the system batches structured operations: merge all dependency declarations first, write `package.json` once, run `npm install` once at the end. - -```yaml -# In manifest.yaml -structured: - npm_dependencies: - whatsapp-web.js: "^2.1.0" - qrcode-terminal: "^0.12.0" - env_additions: - - WHATSAPP_TOKEN - - WHATSAPP_VERIFY_TOKEN - - WHATSAPP_PHONE_ID - docker_compose_services: - whatsapp-redis: - image: redis:alpine - ports: ["6380:6379"] -``` - -### Structured Operation Conflicts - -Structured operations eliminate text merge conflicts but can still conflict at a semantic level: - -- **NPM version conflicts**: two skills request incompatible semver ranges for the same package -- **Port collisions**: two docker-compose services claim the same host port -- **Service name collisions**: two skills define a service with the same name -- **Env var duplicates**: two skills declare the same variable with different expectations - -The resolution policy: - -1. **Automatic where possible**: widen semver ranges to find a compatible version, detect and flag port/name collisions -2. **Level 2 (Claude Code)**: if automatic resolution fails, Claude proposes options based on skill intents -3. **Level 3 (User)**: if it's a genuine product choice (which Redis instance should get port 6379?), ask the user - -Structured operation conflicts are included in the CI overlap graph alongside code file overlaps, so the maintainer test matrix catches these before users encounter them. - -### State Records Structured Outcomes - -`state.yaml` records not just the declared dependencies but the resolved outcomes — actual installed versions, resolved port assignments, final env var list. This makes structured operations replayable and auditable. - -### Deterministic Serialization - -All structured output (YAML, JSON) uses stable serialization: sorted keys, consistent quoting, normalized whitespace. This prevents noisy diffs in git history from non-functional formatting changes. - ---- - -## 3. Skill Package Structure - -A skill contains only the files it adds or modifies. For modified code files, the skill carries the **full modified file** (the clean core with the skill's changes applied). - -``` -skills/ - add-whatsapp/ - SKILL.md # Context, intent, what this skill does and why - manifest.yaml # Metadata, dependencies, env vars, post-apply steps - tests/ # Integration tests for this skill - whatsapp.test.ts - add/ # New files — copied directly - src/channels/whatsapp.ts - src/channels/whatsapp.config.ts - modify/ # Modified code files — merged via git merge-file - src/ - server.ts # Full file: clean core + whatsapp changes - server.ts.intent.md # "Adds WhatsApp webhook route and message handler" - config.ts # Full file: clean core + whatsapp config options - config.ts.intent.md # "Adds WhatsApp channel configuration block" -``` - -### Why Full Modified Files - -- `git merge-file` requires three full files — no intermediate reconstruction step -- Git's three-way merge uses context matching, so it works even if the user has moved code around — unlike line-number-based diffs that break immediately -- Auditable: `diff .nanoclaw/base/src/server.ts skills/add-whatsapp/modify/src/server.ts` shows exactly what the skill changes -- Deterministic: same three inputs always produce the same merge result -- Size is negligible since NanoClaw's core files are small - -### Intent Files - -Each modified code file has a corresponding `.intent.md` with structured headings: - -```markdown -# Intent: server.ts modifications - -## What this skill adds -Adds WhatsApp webhook route and message handler to the Express server. - -## Key sections -- Route registration at `/webhook/whatsapp` (POST and GET for verification) -- Message handler middleware between auth and response pipeline - -## Invariants -- Must not interfere with other channel webhook routes -- Auth middleware must run before the WhatsApp handler -- Error handling must propagate to the global error handler - -## Must-keep sections -- The webhook verification flow (GET route) is required by WhatsApp Cloud API -``` - -Structured headings (What, Key sections, Invariants, Must-keep) give Claude Code specific guidance during conflict resolution instead of requiring it to infer from unstructured text. - -### Manifest Format - -```yaml -# --- Required fields --- -skill: whatsapp -version: 1.2.0 -description: "WhatsApp Business API integration via Cloud API" -core_version: 0.1.0 # The core version this skill was authored against - -# Files this skill adds -adds: - - src/channels/whatsapp.ts - - src/channels/whatsapp.config.ts - -# Code files this skill modifies (three-way merge) -modifies: - - src/server.ts - - src/config.ts - -# File operations (renames, deletes, moves — see Section 5) -file_ops: [] - -# Structured operations (deterministic, no merge — implicit handling) -structured: - npm_dependencies: - whatsapp-web.js: "^2.1.0" - qrcode-terminal: "^0.12.0" - env_additions: - - WHATSAPP_TOKEN - - WHATSAPP_VERIFY_TOKEN - - WHATSAPP_PHONE_ID - -# Skill relationships -conflicts: [] # Skills that cannot coexist without agent resolution -depends: [] # Skills that must be applied first - -# Test command — runs after apply to validate the skill works -test: "npx vitest run src/channels/whatsapp.test.ts" - -# --- Future fields (not yet implemented in v0.1) --- -# author: nanoclaw-team -# license: MIT -# min_skills_system_version: "0.1.0" -# tested_with: [telegram@1.0.0] -# post_apply: [] -``` - -Note: `post_apply` is only for operations that can't be expressed as structured declarations. Dependency installation is **never** in `post_apply` — it's handled implicitly by the structured operations system. - ---- - -## 4. Skills, Customization, and Layering - -### One Skill, One Happy Path - -A skill implements **one way of doing something — the reasonable default that covers 80% of users.** `add-telegram` gives you a clean, solid Telegram integration. It doesn't try to anticipate every use case with predefined configuration options and modes. - -### Customization Is Just More Patching - -The entire system is built around applying transformations to a codebase. Customizing a skill after applying it is no different from any other modification: - -- **Apply the skill** — get the standard Telegram integration -- **Modify from there** — using the customize flow (tracked patch), direct editing (detected by hash tracking), or by applying additional skills that build on top - -### Layered Skills - -Skills can build on other skills: - -``` -add-telegram # Core Telegram integration (happy path) - ├── telegram-reactions # Adds reaction handling (depends: [telegram]) - ├── telegram-multi-bot # Multiple bot instances (depends: [telegram]) - └── telegram-filters # Custom message filtering (depends: [telegram]) -``` - -Each layer is a separate skill with its own `SKILL.md`, manifest (with `depends: [telegram]`), tests, and modified files. The user composes exactly what they want by stacking skills. - -### Custom Skill Application - -A user can apply a skill with their own modifications in a single step: - -1. Apply the skill normally (programmatic merge) -2. Claude Code asks if the user wants to make any modifications -3. User describes what they want different -4. Claude Code makes the modifications on top of the freshly applied skill -5. The modifications are recorded as a custom patch tied to this skill - -Recorded in `state.yaml`: - -```yaml -applied_skills: - - skill: telegram - version: 1.0.0 - custom_patch: .nanoclaw/custom/telegram-group-only.patch - custom_patch_description: "Restrict bot responses to group chats only" -``` - -On replay, the skill applies programmatically, then the custom patch applies on top. - ---- - -## 5. File Operations: Renames, Deletes, Moves - -Core updates and some skills will need to rename, delete, or move files. These are not text merges — they're structural changes handled as explicit scripted operations. - -### Declaration in Manifest - -```yaml -file_ops: - - type: rename - from: src/server.ts - to: src/app.ts - - type: delete - path: src/deprecated/old-handler.ts - - type: move - from: src/utils/helpers.ts - to: src/lib/helpers.ts -``` - -### Execution Order - -File operations run **before** code merges, because merges need to target the correct file paths: - -1. Pre-flight checks (state validation, core version, dependencies, conflicts, drift detection) -2. Acquire operation lock -3. **Backup** all files that will be touched -4. **File operations** (renames, deletes, moves) -5. Copy new files from `add/` -6. Three-way merge modified code files -7. Conflict resolution (rerere auto-resolve, or return with `backupPending: true`) -8. Apply structured operations (npm deps, env vars, docker-compose — batched) -9. Run `npm install` (once, if any structured npm_dependencies exist) -10. Update state (record skill application, file hashes, structured outcomes) -11. Run tests (if `manifest.test` defined; rollback state + backup on failure) -12. Clean up (delete backup on success, release lock) - -### Path Remapping for Skills - -When the core renames a file (e.g., `server.ts` → `app.ts`), skills authored against the old path still reference `server.ts` in their `modifies` and `modify/` directories. **Skill packages are never mutated on the user's machine.** - -Instead, core updates ship a **compatibility map**: - -```yaml -# In the update package -path_remap: - src/server.ts: src/app.ts - src/old-config.ts: src/config/main.ts -``` - -The system resolves paths at apply time: if a skill targets `src/server.ts` and the remap says it's now `src/app.ts`, the merge runs against `src/app.ts`. The remap is recorded in `state.yaml` so future operations are consistent. - -### Safety Checks - -Before executing file operations: - -- Verify the source file exists -- For deletes: warn if the file has modifications beyond the base (user or skill changes would be lost) - ---- - -## 6. The Apply Flow - -When a user runs the skill's slash command in Claude Code: - -### Step 1: Pre-flight Checks - -- Core version compatibility -- Dependencies satisfied -- No unresolvable conflicts with applied skills -- Check for untracked changes (see Section 9) - -### Step 2: Backup - -Copy all files that will be modified to `.nanoclaw/backup/`. If the operation fails at any point, restore from backup. - -### Step 3: File Operations - -Execute renames, deletes, or moves with safety checks. Apply path remapping if needed. - -### Step 4: Apply New Files - -```bash -cp skills/add-whatsapp/add/src/channels/whatsapp.ts src/channels/whatsapp.ts -``` - -### Step 5: Merge Modified Code Files - -For each file in `modifies` (with path remapping applied): - -```bash -git merge-file src/server.ts .nanoclaw/base/src/server.ts skills/add-whatsapp/modify/src/server.ts -``` - -- **Exit code 0**: clean merge, move on -- **Exit code > 0**: conflict markers in file, proceed to resolution - -### Step 6: Conflict Resolution (Three-Level) - -1. **Check shared resolution cache** (`.nanoclaw/resolutions/`) — load into local `git rerere` if a verified resolution exists for this skill combination. **Only apply if input hashes match exactly** (base hash + current hash + skill modified hash). -2. **`git rerere`** — checks local cache. If found, applied automatically. Done. -3. **Claude Code** — reads conflict markers + `SKILL.md` + `.intent.md` (Invariants, Must-keep sections) of current and previously applied skills. Resolves. `git rerere` caches the resolution. -4. **User** — if Claude Code cannot determine intent, it asks the user for the desired behavior. - -### Step 7: Apply Structured Operations - -Collect all structured declarations (from this skill and any previously applied skills if batching). Apply deterministically: - -- Merge npm dependencies into `package.json` (check for version conflicts) -- Append env vars to `.env.example` -- Merge docker-compose services (check for port/name collisions) -- Run `npm install` **once** at the end -- Record resolved outcomes in state - -### Step 8: Post-Apply and Validate - -1. Run any `post_apply` commands (non-structured operations only) -2. Update `.nanoclaw/state.yaml` — skill record, file hashes (base, skill, merged per file), structured outcomes -3. **Run skill tests** — mandatory, even if all merges were clean -4. If tests fail on a clean merge → escalate to Level 2 (Claude Code diagnoses the semantic conflict) - -### Step 9: Clean Up - -If tests pass, delete `.nanoclaw/backup/`. The operation is complete. - -If tests fail and Level 2 can't resolve, restore from `.nanoclaw/backup/` and report the failure. - ---- - -## 7. Shared Resolution Cache - -### The Problem - -`git rerere` is local by default. But NanoClaw has thousands of users applying the same skill combinations. Every user hitting the same conflict and waiting for Claude Code to resolve it is wasteful. - -### The Solution - -NanoClaw maintains a verified resolution cache in `.nanoclaw/resolutions/` that ships with the project. This is the shared artifact — **not** `.git/rr-cache/`, which stays local. - -``` -.nanoclaw/ - resolutions/ - whatsapp@1.2.0+telegram@1.0.0/ - src/ - server.ts.resolution - server.ts.preimage - config.ts.resolution - config.ts.preimage - meta.yaml -``` - -### Hash Enforcement - -A cached resolution is **only applied if input hashes match exactly**: - -```yaml -# meta.yaml -skills: - - whatsapp@1.2.0 - - telegram@1.0.0 -apply_order: [whatsapp, telegram] -core_version: 0.6.0 -resolved_at: 2026-02-15T10:00:00Z -tested: true -test_passed: true -resolution_source: maintainer -input_hashes: - base: "aaa..." - current_after_whatsapp: "bbb..." - telegram_modified: "ccc..." -output_hash: "ddd..." -``` - -If any input hash doesn't match, the cached resolution is skipped and the system proceeds to Level 2. - -### Validated: rerere + merge-file Require an Index Adapter - -`git rerere` does **not** natively recognize `git merge-file` output. This was validated in Phase 0 testing (`tests/phase0-merge-rerere.sh`, 33 tests). - -The issue is not about conflict marker format — `merge-file` uses filenames as labels (`<<<<<<< current.ts`) while `git merge` uses branch names (`<<<<<<< HEAD`), but rerere strips all labels and hashes only the conflict body. The formats are compatible. - -The actual issue: **rerere requires unmerged index entries** (stages 1/2/3) to detect that a merge conflict exists. A normal `git merge` creates these automatically. `git merge-file` operates on the filesystem only and does not touch the index. - -#### The Adapter - -After `git merge-file` produces a conflict, the system must create the index state that rerere expects: - -```bash -# 1. Run the merge (produces conflict markers in the working tree) -git merge-file current.ts .nanoclaw/base/src/file.ts skills/add-whatsapp/modify/src/file.ts - -# 2. If exit code > 0 (conflict), set up rerere adapter: - -# Create blob objects for the three versions -base_hash=$(git hash-object -w .nanoclaw/base/src/file.ts) -ours_hash=$(git hash-object -w skills/previous-skill/modify/src/file.ts) # or the pre-merge current -theirs_hash=$(git hash-object -w skills/add-whatsapp/modify/src/file.ts) - -# Create unmerged index entries at stages 1 (base), 2 (ours), 3 (theirs) -printf '100644 %s 1\tsrc/file.ts\0' "$base_hash" | git update-index --index-info -printf '100644 %s 2\tsrc/file.ts\0' "$ours_hash" | git update-index --index-info -printf '100644 %s 3\tsrc/file.ts\0' "$theirs_hash" | git update-index --index-info - -# Set merge state (rerere checks for MERGE_HEAD) -echo "$(git rev-parse HEAD)" > .git/MERGE_HEAD -echo "skill merge" > .git/MERGE_MSG - -# 3. Now rerere can see the conflict -git rerere # Records preimage, or auto-resolves from cache - -# 4. After resolution (manual or auto): -git add src/file.ts -git rerere # Records postimage (caches the resolution) - -# 5. Clean up merge state -rm .git/MERGE_HEAD .git/MERGE_MSG -git reset HEAD -``` - -#### Key Properties Validated - -- **Conflict body identity**: `merge-file` and `git merge` produce identical conflict bodies for the same inputs. Rerere hashes the body only, so resolutions learned from either source are interchangeable. -- **Hash determinism**: The same conflict always produces the same rerere hash. This is critical for the shared resolution cache. -- **Resolution portability**: Copying `preimage` and `postimage` files (plus the hash directory name) from one repo's `.git/rr-cache/` to another works. Rerere auto-resolves in the target repo. -- **Adjacent line sensitivity**: Changes within ~3 lines of each other are treated as a single conflict hunk by `merge-file`. Skills that modify the same area of a file will conflict even if they modify different lines. This is expected and handled by the resolution cache. - -#### Implication: Git Repository Required - -The adapter requires `git hash-object`, `git update-index`, and `.git/rr-cache/`. This means the project directory must be a git repository for rerere caching to work. Users who download a zip (no `.git/`) lose resolution caching but not functionality — conflicts escalate directly to Level 2 (Claude Code resolves). The system should detect this case and skip rerere operations gracefully. - -### Maintainer Workflow - -When releasing a core update or new skill version: - -1. Fresh codebase at target core version -2. Apply each official skill individually — verify clean merge, run tests -3. Apply pairwise combinations **for skills that modify at least one common file or have overlapping structured operations** -4. Apply curated three-skill stacks based on popularity and high overlap -5. Resolve all conflicts (code and structured) -6. Record all resolutions with input hashes -7. Run full test suite for every combination -8. Ship verified resolutions with the release - -The bar: **a user with any common combination of official skills should never encounter an unresolved conflict.** - ---- - -## 8. State Tracking - -`.nanoclaw/state.yaml` records everything about the installation: - -```yaml -skills_system_version: "0.1.0" # Schema version — tooling checks this before any operation -core_version: 0.1.0 - -applied_skills: - - name: telegram - version: 1.0.0 - applied_at: 2026-02-16T22:47:02.139Z - file_hashes: - src/channels/telegram.ts: "f627b9cf..." - src/channels/telegram.test.ts: "400116769..." - src/config.ts: "9ae28d1f..." - src/index.ts: "46dbe495..." - src/routing.test.ts: "5e1aede9..." - structured_outcomes: - npm_dependencies: - grammy: "^1.39.3" - env_additions: - - TELEGRAM_BOT_TOKEN - - TELEGRAM_ONLY - test: "npx vitest run src/channels/telegram.test.ts" - - - name: discord - version: 1.0.0 - applied_at: 2026-02-17T17:29:37.821Z - file_hashes: - src/channels/discord.ts: "5d669123..." - src/channels/discord.test.ts: "19e1c6b9..." - src/config.ts: "a0a32df4..." - src/index.ts: "d61e3a9d..." - src/routing.test.ts: "edbacb00..." - structured_outcomes: - npm_dependencies: - discord.js: "^14.18.0" - env_additions: - - DISCORD_BOT_TOKEN - - DISCORD_ONLY - test: "npx vitest run src/channels/discord.test.ts" - -custom_modifications: - - description: "Added custom logging middleware" - applied_at: 2026-02-15T12:00:00Z - files_modified: - - src/server.ts - patch_file: .nanoclaw/custom/001-logging-middleware.patch -``` - -**v0.1 implementation notes:** -- `file_hashes` stores a single SHA-256 hash per file (the final merged result). Three-part hashes (base/skill_modified/merged) are planned for a future version to improve drift diagnosis. -- Applied skills use `name` as the key field (not `skill`), matching the TypeScript `AppliedSkill` interface. -- `structured_outcomes` stores the raw manifest values plus the `test` command. Resolved npm versions (actual installed versions vs semver ranges) are not yet tracked. -- Fields like `installed_at`, `last_updated`, `path_remap`, `rebased_at`, `core_version_at_apply`, `files_added`, and `files_modified` are planned for future versions. - ---- - -## 9. Untracked Changes - -If a user edits files directly, the system detects this via hash comparison. - -### When Detection Happens - -Before **any operation that modifies the codebase**: applying a skill, removing a skill, updating the core, replaying, or rebasing. - -### What Happens - -``` -Detected untracked changes to src/server.ts. -[1] Record these as a custom modification (recommended) -[2] Continue anyway (changes preserved, but not tracked for future replay) -[3] Abort -``` - -The system never blocks or loses work. Option 1 generates a patch and records it, making changes reproducible. Option 2 preserves the changes but they won't survive replay. - -### The Recovery Guarantee - -No matter how much a user modifies their codebase outside the system, the three-level model can always bring them back: - -1. **Git**: diff current files against base, identify what changed -2. **Claude Code**: read `state.yaml` to understand what skills were applied, compare against actual file state, identify discrepancies -3. **User**: Claude Code asks what they intended, what to keep, what to discard - -There is no unrecoverable state. - ---- - -## 10. Core Updates - -Core updates must be as programmatic as possible. The NanoClaw team is responsible for ensuring updates apply cleanly to common skill combinations. - -### Patches and Migrations - -Most core changes — bug fixes, performance improvements, new functionality — propagate automatically through the three-way merge. No special handling needed. - -**Breaking changes** — changed defaults, removed features, functionality moved to skills — require a **migration**. A migration is a skill that preserves the old behavior, authored against the new core. It's applied automatically during the update so the user's setup doesn't change. - -The maintainer's responsibility when making a breaking change: make the change in core, author a migration skill that reverts it, add the entry to `migrations.yaml`, test it. That's the cost of breaking changes. - -### `migrations.yaml` - -An append-only file in the repo root. Each entry records a breaking change and the skill that preserves the old behavior: - -```yaml -- since: 0.6.0 - skill: apple-containers@1.0.0 - description: "Preserves Apple Containers (default changed to Docker in 0.6)" - -- since: 0.7.0 - skill: add-whatsapp@2.0.0 - description: "Preserves WhatsApp (moved from core to skill in 0.7)" - -- since: 0.8.0 - skill: legacy-auth@1.0.0 - description: "Preserves legacy auth module (removed from core in 0.8)" -``` - -Migration skills are regular skills in the `skills/` directory. They have manifests, intent files, tests — everything. They're authored against the **new** core version: the modified file is the new core with the specific breaking change reverted, everything else (bug fixes, new features) identical to the new core. - -### How Migrations Work During Updates - -1. Three-way merge brings in everything from the new core — patches, breaking changes, all of it -2. Conflict resolution (normal) -3. Re-apply custom patches (normal) -4. **Update base to new core** -5. Filter `migrations.yaml` for entries where `since` > user's old `core_version` -6. **Apply each migration skill using the normal apply flow against the new base** -7. Record migration skills in `state.yaml` like any other skill -8. Run tests - -Step 6 is just the same apply function used for any skill. The migration skill merges against the new base: - -- **Base**: new core (e.g., v0.8 with Docker) -- **Current**: user's file after the update merge (new core + user's customizations preserved by the earlier merge) -- **Other**: migration skill's file (new core with Docker reverted to Apple, everything else identical) - -Three-way merge correctly keeps user's customizations, reverts the breaking change, and preserves all bug fixes. If there's a conflict, normal resolution: cache → Claude → user. - -For big version jumps (v0.5 → v0.8), all applicable migrations are applied in sequence. Migration skills are maintained against the latest core version, so they always compose correctly with the current codebase. - -### What the User Sees - -``` -Core updated: 0.5.0 → 0.8.0 - ✓ All patches applied - - Preserving your current setup: - + apple-containers@1.0.0 - + add-whatsapp@2.0.0 - + legacy-auth@1.0.0 - - Skill updates: - ✓ add-telegram 1.0.0 → 1.2.0 - - To accept new defaults: /remove-skill - ✓ All tests passing -``` - -No prompts, no choices during the update. The user's setup doesn't change. If they later want to accept a new default, they remove the migration skill. - -### What the Core Team Ships With an Update - -``` -updates/ - 0.5.0-to-0.6.0/ - migration.md # What changed, why, and how it affects skills - files/ # The new core files - file_ops: # Any renames, deletes, moves - path_remap: # Compatibility map for old skill paths - resolutions/ # Pre-computed resolutions for official skills -``` - -Plus any new migration skills added to `skills/` and entries appended to `migrations.yaml`. - -### The Maintainer's Process - -1. **Make the core change** -2. **If it's a breaking change**: author a migration skill against the new core, add entry to `migrations.yaml` -3. **Write `migration.md`** — what changed, why, what skills might be affected -4. **Test every official skill individually** against the new core (including migration skills) -5. **Test pairwise combinations** for skills that share modified files or structured operations -6. **Test curated three-skill stacks** based on popularity and overlap -7. **Resolve all conflicts** -8. **Record all resolutions** with enforced input hashes -9. **Run full test suites** -10. **Ship everything** — migration guide, migration skills, file ops, path remap, resolutions - -The bar: **patches apply silently. Breaking changes are auto-preserved via migration skills. A user should never be surprised by a change to their working setup.** - -### Update Flow (Full) - -#### Step 1: Pre-flight - -- Check for untracked changes -- Read `state.yaml` -- Load shipped resolutions -- Parse `migrations.yaml`, filter for applicable migrations - -#### Step 2: Preview - -Before modifying anything, show the user what's coming. This uses only git commands — no files are opened or changed: - -```bash -# Compute common base -BASE=$(git merge-base HEAD upstream/$BRANCH) - -# Upstream commits since last sync -git log --oneline $BASE..upstream/$BRANCH - -# Files changed upstream -git diff --name-only $BASE..upstream/$BRANCH -``` - -Present a summary grouped by impact: - -``` -Update available: 0.5.0 → 0.8.0 (12 commits) - - Source: 4 files modified (server.ts, config.ts, ...) - Skills: 2 new skills added, 1 skill updated - Config: package.json, docker-compose.yml updated - - Migrations (auto-applied to preserve your setup): - + apple-containers@1.0.0 (container default changed to Docker) - + add-whatsapp@2.0.0 (WhatsApp moved from core to skill) - - Skill updates: - add-telegram 1.0.0 → 1.2.0 - - [1] Proceed with update - [2] Abort -``` - -If the user aborts, stop here. Nothing was modified. - -#### Step 3: Backup - -Copy all files that will be modified to `.nanoclaw/backup/`. - -#### Step 4: File Operations and Path Remap - -Apply renames, deletes, moves. Record path remap in state. - -#### Step 5: Three-Way Merge - -For each core file that changed: - -```bash -git merge-file src/server.ts .nanoclaw/base/src/server.ts updates/0.5.0-to-0.6.0/files/src/server.ts -``` - -#### Step 6: Conflict Resolution - -1. Shipped resolutions (hash-verified) → automatic -2. `git rerere` local cache → automatic -3. Claude Code with `migration.md` + skill intents → resolves -4. User → only for genuine ambiguity - -#### Step 7: Re-apply Custom Patches - -```bash -git apply --3way .nanoclaw/custom/001-logging-middleware.patch -``` - -Using `--3way` allows git to fall back to three-way merge when line numbers have drifted. If `--3way` fails, escalate to Level 2. - -#### Step 8: Update Base - -`.nanoclaw/base/` replaced with new clean core. This is the **only time** the base changes. - -#### Step 9: Apply Migration Skills - -For each applicable migration (where `since` > old `core_version`), apply the migration skill using the normal apply flow against the new base. Record in `state.yaml`. - -#### Step 10: Re-apply Updated Skills - -Skills live in the repo and update alongside core files. After the update, compare the version in each skill's `manifest.yaml` on disk against the version recorded in `state.yaml`. - -For each skill where the on-disk version is newer than the recorded version: - -1. Re-apply the skill using the normal apply flow against the new base -2. The three-way merge brings in the skill's new changes while preserving user customizations -3. Re-apply any custom patches tied to the skill (`git apply --3way`) -4. Update the version in `state.yaml` - -Skills whose version hasn't changed are skipped — no action needed. - -If the user has a custom patch on a skill that changed significantly, the patch may conflict. Normal resolution: cache → Claude → user. - -#### Step 11: Re-run Structured Operations - -Recompute structured operations against the updated codebase to ensure consistency. - -#### Step 12: Validate - -- Run all skill tests — mandatory -- Compatibility report: - -``` -Core updated: 0.5.0 → 0.8.0 - ✓ All patches applied - - Migrations: - + apple-containers@1.0.0 (preserves container runtime) - + add-whatsapp@2.0.0 (WhatsApp moved to skill) - - Skill updates: - ✓ add-telegram 1.0.0 → 1.2.0 (new features applied) - ✓ custom/telegram-group-only — re-applied cleanly - - ✓ All tests passing -``` - -#### Step 13: Clean Up - -Delete `.nanoclaw/backup/`. - -### Progressive Core Slimming - -Migrations enable a clean path for slimming down the core over time. Each release can move more functionality to skills: - -- The breaking change removes the feature from core -- The migration skill preserves it for existing users -- New users start with a minimal core and add what they need -- Over time, `state.yaml` reflects exactly what each user is running - ---- - -## 11. Skill Removal (Uninstall) - -Removing a skill is not a reverse-patch operation. **Uninstall is a replay without the skill.** - -### How It Works - -1. Read `state.yaml` to get the full list of applied skills and custom modifications -2. Remove the target skill from the list -3. Backup the current codebase to `.nanoclaw/backup/` -4. **Replay from clean base** — apply each remaining skill in order, apply custom patches, using the resolution cache -5. Run all tests -6. If tests pass, delete backup and update `state.yaml` -7. If tests fail, restore from backup and report - -### Custom Patches Tied to the Removed Skill - -If the removed skill has a `custom_patch` in `state.yaml`, the user is warned: - -``` -Removing telegram will also discard custom patch: "Restrict bot responses to group chats only" -[1] Continue (discard custom patch) -[2] Abort -``` - ---- - -## 12. Rebase - -Flatten accumulated layers into a clean starting point. - -### What Rebase Does - -1. Takes the user's current actual files as the new reality -2. Updates `.nanoclaw/base/` to the current core version's clean files -3. For each applied skill, regenerates the modified file diffs against the new base -4. Updates `state.yaml` with `rebased_at` timestamp -5. Clears old custom patches (now baked in) -6. Clears stale resolution cache entries - -### When to Rebase - -- After a major core update -- When accumulated patches become unwieldy -- Before a significant new skill application -- Periodically as maintenance - -### Tradeoffs - -**Lose**: individual skill patch history, ability to cleanly remove a single old skill, old custom patches as separate artifacts - -**Gain**: clean base, simpler future merges, reduced cache size, fresh starting point - ---- - -## 13. Replay - -Given `state.yaml`, reproduce the exact installation on a fresh machine with no AI intervention (assuming all resolutions are cached). - -### Replay Flow - -```bash -# Fully programmatic — no Claude Code needed - -# 1. Install core at specified version -nanoclaw-init --version 0.5.0 - -# 2. Load shared resolutions into local rerere cache -load-resolutions .nanoclaw/resolutions/ - -# 3. For each skill in applied_skills (in order): -for skill in state.applied_skills: - # File operations - apply_file_ops(skill) - - # Copy new files - cp skills/${skill.name}/add/* . - - # Merge modified code files (with path remapping) - for file in skill.files_modified: - resolved_path = apply_remap(file, state.path_remap) - git merge-file ${resolved_path} .nanoclaw/base/${resolved_path} skills/${skill.name}/modify/${file} - # git rerere auto-resolves from shared cache if needed - - # Apply skill-specific custom patch if recorded - if skill.custom_patch: - git apply --3way ${skill.custom_patch} - -# 4. Apply all structured operations (batched) -collect_all_structured_ops(state.applied_skills) -merge_npm_dependencies → write package.json once -npm install once -merge_env_additions → write .env.example once -merge_compose_services → write docker-compose.yml once - -# 5. Apply standalone custom modifications -for custom in state.custom_modifications: - git apply --3way ${custom.patch_file} - -# 6. Run tests and verify hashes -run_tests && verify_hashes -``` - ---- - -## 14. Skill Tests - -Each skill includes integration tests that validate the skill works correctly when applied. - -### Structure - -``` -skills/ - add-whatsapp/ - tests/ - whatsapp.test.ts -``` - -### What Tests Validate - -- **Single skill on fresh core**: apply to clean codebase → tests pass → integration works -- **Skill functionality**: the feature actually works -- **Post-apply state**: files in expected state, `state.yaml` correctly updated - -### When Tests Run (Always) - -- **After applying a skill** — even if all merges were clean -- **After core update** — even if all merges were clean -- **After uninstall replay** — confirms removal didn't break remaining skills -- **In CI** — tests all official skills individually and in common combinations -- **During replay** — validates replayed state - -Clean merge ≠ working code. Tests are the only reliable signal. - -### CI Test Matrix - -Test coverage is **smart, not exhaustive**: - -- Every official skill individually against each supported core version -- **Pairwise combinations for skills that modify at least one common file or have overlapping structured operations** -- Curated three-skill stacks based on popularity and high overlap -- Test matrix auto-generated from manifest `modifies` and `structured` fields - -Each passing combination generates a verified resolution entry for the shared cache. - ---- - -## 15. Project Configuration - -### `.gitattributes` - -Ship with NanoClaw to reduce noisy merge conflicts: - -``` -* text=auto -*.ts text eol=lf -*.json text eol=lf -*.yaml text eol=lf -*.md text eol=lf -``` - ---- - -## 16. Directory Structure - -``` -project/ - src/ # The actual codebase - server.ts - config.ts - channels/ - whatsapp.ts - telegram.ts - skills/ # Skill packages (Claude Code slash commands) - add-whatsapp/ - SKILL.md - manifest.yaml - tests/ - whatsapp.test.ts - add/ - src/channels/whatsapp.ts - modify/ - src/ - server.ts - server.ts.intent.md - config.ts - config.ts.intent.md - add-telegram/ - ... - telegram-reactions/ # Layered skill - ... - .nanoclaw/ - base/ # Clean core (shared base) - src/ - server.ts - config.ts - ... - state.yaml # Full installation state - backup/ # Temporary backup during operations - custom/ # Custom patches - telegram-group-only.patch - 001-logging-middleware.patch - 001-logging-middleware.md - resolutions/ # Shared verified resolution cache - whatsapp@1.2.0+telegram@1.0.0/ - src/ - server.ts.resolution - server.ts.preimage - meta.yaml - .gitattributes -``` - ---- - -## 17. Design Principles - -1. **Use git, don't reinvent it.** `git merge-file` for code merges, `git rerere` for caching resolutions, `git apply --3way` for custom patches. -2. **Three-level resolution: git → Claude → user.** Programmatic first, AI second, human third. -3. **Clean merges aren't enough.** Tests run after every operation. Semantic conflicts survive text merges. -4. **All operations are safe.** Backup before, restore on failure. No half-applied state. -5. **One shared base.** `.nanoclaw/base/` is the clean core before any skills or customizations. It's the stable common ancestor for all three-way merges. Only updated on core updates. -6. **Code merges vs. structured operations.** Source code is three-way merged. Dependencies, env vars, and configs are aggregated programmatically. Structured operations are implicit and batched. -7. **Resolutions are learned and shared.** Maintainers resolve conflicts and ship verified resolutions with hash enforcement. `.nanoclaw/resolutions/` is the shared artifact. -8. **One skill, one happy path.** No predefined configuration options. Customization is more patching. -9. **Skills layer and compose.** Core skills provide the foundation. Extension skills add capabilities. -10. **Intent is first-class and structured.** `SKILL.md`, `.intent.md` (What, Invariants, Must-keep), and `migration.md`. -11. **State is explicit and complete.** Skills, custom patches, per-file hashes, structured outcomes, path remaps. Replay is deterministic. Drift is instant to detect. -12. **Always recoverable.** The three-level model reconstructs coherent state from any starting point. -13. **Uninstall is replay.** Replay from clean base without the skill. Backup for safety. -14. **Core updates are the maintainers' responsibility.** Test, resolve, ship. Breaking changes require a migration skill that preserves the old behavior. The cost of a breaking change is authoring and testing the migration. Users should never be surprised by a change to their setup. -15. **File operations and path remapping are first-class.** Renames, deletes, moves in manifests. Skills are never mutated — paths resolve at apply time. -16. **Skills are tested.** Integration tests per skill. CI tests pairwise by overlap. Tests run always. -17. **Deterministic serialization.** Sorted keys, consistent formatting. No noisy diffs. -18. **Rebase when needed.** Flatten layers for a clean starting point. -19. **Progressive core slimming.** Breaking changes move functionality from core to migration skills. Existing users keep what they have automatically. New users start minimal and add what they need. \ No newline at end of file diff --git a/docs/nanorepo-architecture.md b/docs/nanorepo-architecture.md deleted file mode 100644 index 1365e9e..0000000 --- a/docs/nanorepo-architecture.md +++ /dev/null @@ -1,168 +0,0 @@ -# NanoClaw Skills Architecture - -## What Skills Are For - -NanoClaw's core is intentionally minimal. Skills are how users extend it: adding channels, integrations, cross-platform support, or replacing internals entirely. Examples: add Telegram alongside WhatsApp, switch from Apple Container to Docker, add Gmail integration, add voice message transcription. Each skill modifies the actual codebase, adding channel handlers, updating the message router, changing container configuration, and adding dependencies, rather than working through a plugin API or runtime hooks. - -## Why This Architecture - -The problem: users need to combine multiple modifications to a shared codebase, keep those modifications working across core updates, and do all of this without becoming git experts or losing their custom changes. A plugin system would be simpler but constrains what skills can do. Giving skills full codebase access means they can change anything, but that creates merge conflicts, update breakage, and state tracking challenges. - -This architecture solves that by making skill application fully programmatic using standard git mechanics, with AI as a fallback for conflicts git can't resolve, and a shared resolution cache so most users never hit those conflicts at all. The result: users compose exactly the features they want, customizations survive core updates automatically, and the system is always recoverable. - -## Core Principle - -Skills are self-contained, auditable packages applied via standard git merge mechanics. Claude Code orchestrates the process — running git commands, reading skill manifests, and stepping in only when git can't resolve a conflict. The system uses existing git features (`merge-file`, `rerere`, `apply`) rather than custom merge infrastructure. - -## Three-Level Resolution Model - -Every operation follows this escalation: - -1. **Git** — deterministic. `git merge-file` merges, `git rerere` replays cached resolutions, structured operations apply without merging. No AI. Handles the vast majority of cases. -2. **Claude Code** — reads `SKILL.md`, `.intent.md`, and `state.yaml` to resolve conflicts git can't handle. Caches resolutions via `git rerere` so the same conflict never needs resolving twice. -3. **Claude Code + user input** — when Claude Code lacks sufficient context to determine intent (e.g., two features genuinely conflict at an application level), it asks the user for a decision, then uses that input to perform the resolution. Claude Code still does the work — the user provides direction, not code. - -**Important**: A clean merge doesn't guarantee working code. Semantic conflicts can produce clean text merges that break at runtime. **Tests run after every operation.** - -## Backup/Restore Safety - -Before any operation, all affected files are copied to `.nanoclaw/backup/`. On success, backup is deleted. On failure, backup is restored. Works safely for users who don't use git. - -## The Shared Base - -`.nanoclaw/base/` holds a clean copy of the core codebase. This is the single common ancestor for all three-way merges, only updated during core updates. - -## Two Types of Changes - -### Code Files (Three-Way Merge) -Source code where skills weave in logic. Merged via `git merge-file` against the shared base. Skills carry full modified files. - -### Structured Data (Deterministic Operations) -Files like `package.json`, `docker-compose.yml`, `.env.example`. Skills declare requirements in the manifest; the system applies them programmatically. Multiple skills' declarations are batched — dependencies merged, `package.json` written once, `npm install` run once. - -```yaml -structured: - npm_dependencies: - whatsapp-web.js: "^2.1.0" - env_additions: - - WHATSAPP_TOKEN - docker_compose_services: - whatsapp-redis: - image: redis:alpine - ports: ["6380:6379"] -``` - -Structured conflicts (version incompatibilities, port collisions) follow the same three-level resolution model. - -## Skill Package Structure - -A skill contains only the files it adds or modifies. Modified code files carry the **full file** (clean core + skill's changes), making `git merge-file` straightforward and auditable. - -``` -skills/add-whatsapp/ - SKILL.md # What this skill does and why - manifest.yaml # Metadata, dependencies, structured ops - tests/whatsapp.test.ts # Integration tests - add/src/channels/whatsapp.ts # New files - modify/src/server.ts # Full modified file for merge - modify/src/server.ts.intent.md # Structured intent for conflict resolution -``` - -### Intent Files -Each modified file has a `.intent.md` with structured headings: **What this skill adds**, **Key sections**, **Invariants**, and **Must-keep sections**. These give Claude Code specific guidance during conflict resolution. - -### Manifest -Declares: skill metadata, core version compatibility, files added/modified, file operations, structured operations, skill relationships (conflicts, depends, tested_with), post-apply commands, and test command. - -## Customization and Layering - -**One skill, one happy path** — a skill implements the reasonable default for 80% of users. - -**Customization is more patching.** Apply the skill, then modify via tracked patches, direct editing, or additional layered skills. Custom modifications are recorded in `state.yaml` and replayable. - -**Skills layer via `depends`.** Extension skills build on base skills (e.g., `telegram-reactions` depends on `add-telegram`). - -## File Operations - -Renames, deletes, and moves are declared in the manifest and run **before** code merges. When core renames a file, a **path remap** resolves skill references at apply time — skill packages are never mutated. - -## The Apply Flow - -1. Pre-flight checks (compatibility, dependencies, untracked changes) -2. Backup -3. File operations + path remapping -4. Copy new files -5. Merge modified code files (`git merge-file`) -6. Conflict resolution (shared cache → `git rerere` → Claude Code → Claude Code + user input) -7. Apply structured operations (batched) -8. Post-apply commands, update `state.yaml` -9. **Run tests** (mandatory, even if all merges were clean) -10. Clean up (delete backup on success, restore on failure) - -## Shared Resolution Cache - -`.nanoclaw/resolutions/` ships pre-computed, verified conflict resolutions with **hash enforcement** — a cached resolution only applies if base, current, and skill input hashes match exactly. This means most users never encounter unresolved conflicts for common skill combinations. - -### rerere Adapter -`git rerere` requires unmerged index entries that `git merge-file` doesn't create. An adapter sets up the required index state after `merge-file` produces a conflict, enabling rerere caching. This requires the project to be a git repository; users without `.git/` lose caching but not functionality. - -## State Tracking - -`.nanoclaw/state.yaml` records: core version, all applied skills (with per-file hashes for base/skill/merged), structured operation outcomes, custom patches, and path remaps. This makes drift detection instant and replay deterministic. - -## Untracked Changes - -Direct edits are detected via hash comparison before any operation. Users can record them as tracked patches, continue untracked, or abort. The three-level model can always recover coherent state from any starting point. - -## Core Updates - -Most changes propagate automatically through three-way merge. **Breaking changes** require a **migration skill** — a regular skill that preserves the old behavior, authored against the new core. Migrations are declared in `migrations.yaml` and applied automatically during updates. - -### Update Flow -1. Preview changes (git-only, no files modified) -2. Backup → file operations → three-way merge → conflict resolution -3. Re-apply custom patches (`git apply --3way`) -4. **Update base** to new core -5. Apply migration skills (preserves user's setup automatically) -6. Re-apply updated skills (version-changed skills only) -7. Re-run structured operations → run all tests → clean up - -The user sees no prompts during updates. To accept a new default later, they remove the migration skill. - -## Skill Removal - -Uninstall is **replay without the skill**: read `state.yaml`, remove the target skill, replay all remaining skills from clean base using the resolution cache. Backup for safety. - -## Rebase - -Flatten accumulated layers into a clean starting point. Updates base, regenerates diffs, clears old patches and stale cache entries. Trades individual skill history for simpler future merges. - -## Replay - -Given `state.yaml`, reproduce the exact installation on a fresh machine with no AI (assuming cached resolutions). Apply skills in order, merge, apply custom patches, batch structured operations, run tests. - -## Skill Tests - -Each skill includes integration tests. Tests run **always** — after apply, after update, after uninstall, during replay, in CI. CI tests all official skills individually and pairwise combinations for skills sharing modified files or structured operations. - -## Design Principles - -1. **Use git, don't reinvent it.** -2. **Three-level resolution: git → Claude Code → Claude Code + user input.** -3. **Clean merges aren't enough.** Tests run after every operation. -4. **All operations are safe.** Backup/restore, no half-applied state. -5. **One shared base**, only updated on core updates. -6. **Code merges vs. structured operations.** Source code is merged; configs are aggregated. -7. **Resolutions are learned and shared** with hash enforcement. -8. **One skill, one happy path.** Customization is more patching. -9. **Skills layer and compose.** -10. **Intent is first-class and structured.** -11. **State is explicit and complete.** Replay is deterministic. -12. **Always recoverable.** -13. **Uninstall is replay.** -14. **Core updates are the maintainers' responsibility.** Breaking changes require migration skills. -15. **File operations and path remapping are first-class.** -16. **Skills are tested.** CI tests pairwise by overlap. -17. **Deterministic serialization.** No noisy diffs. -18. **Rebase when needed.** -19. **Progressive core slimming** via migration skills. \ No newline at end of file diff --git a/docs/skills-as-branches.md b/docs/skills-as-branches.md index e1cace4..4a6db9b 100644 --- a/docs/skills-as-branches.md +++ b/docs/skills-as-branches.md @@ -2,7 +2,20 @@ ## Overview -NanoClaw skills are distributed as git branches on the upstream repository. Applying a skill is a `git merge`. Updating core is a `git merge`. Everything is standard git. +This document covers **feature skills** — skills that add capabilities via git branch merges. This is the most complex skill type and the primary way NanoClaw is extended. + +NanoClaw has four types of skills overall. See [CONTRIBUTING.md](../CONTRIBUTING.md) for the full taxonomy: + +| Type | Location | How it works | +|------|----------|-------------| +| **Feature** (this doc) | `.claude/skills/` + `skill/*` branch | SKILL.md has instructions; code lives on a branch, applied via `git merge` | +| **Utility** | `.claude/skills//` with code files | Self-contained tools; code in skill directory, copied into place on install | +| **Operational** | `.claude/skills/` on `main` | Instruction-only workflows (setup, debug, update) | +| **Container** | `container/skills/` | Loaded inside agent containers at runtime | + +--- + +Feature skills are distributed as git branches on the upstream repository. Applying a skill is a `git merge`. Updating core is a `git merge`. Everything is standard git. This replaces the previous `skills-engine/` system (three-way file merging, `.nanoclaw/` state, manifest files, replay, backup/restore) with plain git operations and Claude for conflict resolution. @@ -310,7 +323,9 @@ Standard fork contribution workflow. Their custom changes stay on their main and ## Contributing a Skill -### Contributor flow +The flow below is for **feature skills** (branch-based). For utility skills (self-contained tools) and container skills, the contributor opens a PR that adds files directly to `.claude/skills//` or `container/skills//` — no branch extraction needed. See [CONTRIBUTING.md](../CONTRIBUTING.md) for all skill types. + +### Contributor flow (feature skills) 1. Fork `qwibitai/nanoclaw` 2. Branch from `main` diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..fa257de --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,32 @@ +import globals from 'globals' +import pluginJs from '@eslint/js' +import tseslint from 'typescript-eslint' +import noCatchAll from 'eslint-plugin-no-catch-all' + +export default [ + { ignores: ['node_modules/', 'dist/', 'container/', 'groups/'] }, + { files: ['src/**/*.{js,ts}'] }, + { languageOptions: { globals: globals.node } }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + { + plugins: { 'no-catch-all': noCatchAll }, + rules: { + 'preserve-caught-error': ['error', { requireCatchParameter: true }], + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'all', + argsIgnorePattern: '^_', + caughtErrors: 'all', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_', + ignoreRestSiblings: true, + }, + ], + 'no-catch-all/no-catch-all': 'warn', + '@typescript-eslint/no-explicit-any': 'warn', + }, + }, +] diff --git a/groups/global/CLAUDE.md b/groups/global/CLAUDE.md index 28e97a7..c814e39 100644 --- a/groups/global/CLAUDE.md +++ b/groups/global/CLAUDE.md @@ -49,10 +49,28 @@ When you learn something important: ## Message Formatting -NEVER use markdown. Only use WhatsApp/Telegram formatting: -- *single asterisks* for bold (NEVER **double asterisks**) -- _underscores_ for italic -- • bullet points -- ```triple backticks``` for code +Format messages based on the channel you're responding to. Check your group folder name: -No ## headings. No [links](url). No **double stars**. +### Slack channels (folder starts with `slack_`) + +Use Slack mrkdwn syntax. Run `/slack-formatting` for the full reference. Key rules: +- `*bold*` (single asterisks) +- `_italic_` (underscores) +- `` for links (NOT `[text](url)`) +- `•` bullets (no numbered lists) +- `:emoji:` shortcodes +- `>` for block quotes +- No `##` headings — use `*Bold text*` instead + +### WhatsApp/Telegram channels (folder starts with `whatsapp_` or `telegram_`) + +- `*bold*` (single asterisks, NEVER **double**) +- `_italic_` (underscores) +- `•` bullet points +- ` ``` ` code blocks + +No `##` headings. No `[links](url)`. No `**double stars**`. + +### Discord channels (folder starts with `discord_`) + +Standard Markdown works: `**bold**`, `*italic*`, `[links](url)`, `# headings`. diff --git a/groups/main/CLAUDE.md b/groups/main/CLAUDE.md index 11e846b..d4e3258 100644 --- a/groups/main/CLAUDE.md +++ b/groups/main/CLAUDE.md @@ -43,15 +43,33 @@ When you learn something important: - Split files larger than 500 lines into folders - Keep an index in your memory for the files you create -## WhatsApp Formatting (and other messaging apps) +## Message Formatting -Do NOT use markdown headings (##) in WhatsApp messages. Only use: -- *Bold* (single asterisks) (NEVER **double asterisks**) -- _Italic_ (underscores) -- • Bullets (bullet points) -- ```Code blocks``` (triple backticks) +Format messages based on the channel. Check the group folder name prefix: -Keep messages clean and readable for WhatsApp. +### Slack channels (folder starts with `slack_`) + +Use Slack mrkdwn syntax. Run `/slack-formatting` for the full reference. Key rules: +- `*bold*` (single asterisks) +- `_italic_` (underscores) +- `` for links (NOT `[text](url)`) +- `•` bullets (no numbered lists) +- `:emoji:` shortcodes like `:white_check_mark:`, `:rocket:` +- `>` for block quotes +- No `##` headings — use `*Bold text*` instead + +### WhatsApp/Telegram (folder starts with `whatsapp_` or `telegram_`) + +- `*bold*` (single asterisks, NEVER **double**) +- `_italic_` (underscores) +- `•` bullet points +- ` ``` ` code blocks + +No `##` headings. No `[links](url)`. No `**double stars**`. + +### Discord (folder starts with `discord_`) + +Standard Markdown: `**bold**`, `*italic*`, `[links](url)`, `# headings`. --- diff --git a/package-lock.json b/package-lock.json index 261f226..fae72c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nanoclaw", - "version": "1.2.19", + "version": "1.2.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nanoclaw", - "version": "1.2.19", + "version": "1.2.21", "dependencies": { "better-sqlite3": "^11.8.1", "cron-parser": "^5.5.0", @@ -16,13 +16,18 @@ "zod": "^4.3.6" }, "devDependencies": { + "@eslint/js": "^9.35.0", "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.10.0", "@vitest/coverage-v8": "^4.0.18", + "eslint": "^9.35.0", + "eslint-plugin-no-catch-all": "^1.1.0", + "globals": "^15.12.0", "husky": "^9.1.7", "prettier": "^3.8.1", "tsx": "^4.19.0", "typescript": "^5.7.0", + "typescript-eslint": "^8.35.0", "vitest": "^4.0.18" }, "engines": { @@ -531,6 +536,228 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -957,6 +1184,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.19.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", @@ -967,6 +1201,288 @@ "undici-types": "~6.21.0" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", + "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/type-utils": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", + "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", + "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.1", + "@typescript-eslint/types": "^8.57.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", + "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", + "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", + "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", + "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", + "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.1", + "@typescript-eslint/tsconfig-utils": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", + "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", + "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitest/coverage-v8": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", @@ -1109,6 +1625,69 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1140,6 +1719,13 @@ "node": ">=8.0.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1191,6 +1777,17 @@ "readable-stream": "^3.4.0" } }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1215,6 +1812,16 @@ "ieee754": "^1.1.13" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -1225,18 +1832,62 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "license": "MIT" }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/cron-parser": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-5.5.0.tgz", @@ -1249,6 +1900,21 @@ "node": ">=18" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -1258,6 +1924,24 @@ "node": "*" } }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -1282,6 +1966,13 @@ "node": ">=4.0.0" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1349,6 +2040,173 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-no-catch-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-catch-all/-/eslint-plugin-no-catch-all-1.1.0.tgz", + "integrity": "sha512-VkP62jLTmccPrFGN/W6V7a3SEwdtTZm+Su2k4T3uyJirtkm0OMMm97h7qd8pRFAHus/jQg9FpUpLRc7sAylBEQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=2.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1359,6 +2217,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1384,6 +2252,27 @@ "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -1408,12 +2297,63 @@ } } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -1454,6 +2394,32 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1513,6 +2479,43 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1525,6 +2528,36 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -1580,6 +2613,87 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/luxon": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", @@ -1639,6 +2753,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -1654,6 +2781,13 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1679,6 +2813,13 @@ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-abi": { "version": "3.87.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", @@ -1720,6 +2861,89 @@ "wrappy": "1" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -1872,6 +3096,16 @@ "node": ">=10" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", @@ -1914,6 +3148,16 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/quick-format-unescaped": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", @@ -1967,6 +3211,16 @@ "node": ">= 12.13.0" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -2079,6 +3333,29 @@ "node": ">=10" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -2288,6 +3565,19 @@ "node": ">=14.0.0" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -2320,6 +3610,19 @@ "node": "*" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2334,6 +3637,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", + "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.1", + "@typescript-eslint/parser": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -2341,6 +3668,16 @@ "dev": true, "license": "MIT" }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2500,6 +3837,22 @@ } } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -2517,6 +3870,16 @@ "node": ">=8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2538,6 +3901,19 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/package.json b/package.json index beacf9b..b30dd39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "1.2.19", + "version": "1.2.21", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "main": "dist/index.js", @@ -15,6 +15,8 @@ "prepare": "husky", "setup": "tsx setup/index.ts", "auth": "tsx src/whatsapp-auth.ts", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", "test": "vitest run", "test:watch": "vitest" }, @@ -27,13 +29,18 @@ "zod": "^4.3.6" }, "devDependencies": { + "@eslint/js": "^9.35.0", "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.10.0", "@vitest/coverage-v8": "^4.0.18", + "eslint": "^9.35.0", + "eslint-plugin-no-catch-all": "^1.1.0", + "globals": "^15.12.0", "husky": "^9.1.7", "prettier": "^3.8.1", "tsx": "^4.19.0", "typescript": "^5.7.0", + "typescript-eslint": "^8.35.0", "vitest": "^4.0.18" }, "engines": { diff --git a/repo-tokens/badge.svg b/repo-tokens/badge.svg index b268ecc..993856e 100644 --- a/repo-tokens/badge.svg +++ b/repo-tokens/badge.svg @@ -1,5 +1,5 @@ - - 40.7k tokens, 20% of context window + + 40.9k tokens, 20% of context window @@ -15,8 +15,8 @@ tokens - - 40.7k + + 40.9k diff --git a/src/channels/registry.test.ts b/src/channels/registry.test.ts index e47b1bf..e89f62b 100644 --- a/src/channels/registry.test.ts +++ b/src/channels/registry.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { registerChannel, diff --git a/src/container-runner.ts b/src/container-runner.ts index 59bccd8..a6b58d7 100644 --- a/src/container-runner.ts +++ b/src/container-runner.ts @@ -507,11 +507,7 @@ export async function runContainerAgent( // Full input is only included at verbose level to avoid // persisting user conversation content on every non-zero exit. if (isVerbose) { - logLines.push( - `=== Input ===`, - JSON.stringify(input, null, 2), - ``, - ); + logLines.push(`=== Input ===`, JSON.stringify(input, null, 2), ``); } else { logLines.push( `=== Input Summary ===`, @@ -698,7 +694,7 @@ export function writeGroupsSnapshot( groupFolder: string, isMain: boolean, groups: AvailableGroup[], - registeredJids: Set, + _registeredJids: Set, ): void { const groupIpcDir = resolveGroupIpcPath(groupFolder); fs.mkdirSync(groupIpcDir, { recursive: true }); diff --git a/src/container-runtime.ts b/src/container-runtime.ts index 5a4f91e..9f32d10 100644 --- a/src/container-runtime.ts +++ b/src/container-runtime.ts @@ -96,7 +96,9 @@ export function ensureContainerRuntimeRunning(): void { console.error( '╚════════════════════════════════════════════════════════════════╝\n', ); - throw new Error('Container runtime is required but failed to start'); + throw new Error('Container runtime is required but failed to start', { + cause: err, + }); } } diff --git a/src/group-queue.test.ts b/src/group-queue.test.ts index ca2702a..d7de517 100644 --- a/src/group-queue.test.ts +++ b/src/group-queue.test.ts @@ -40,7 +40,7 @@ describe('GroupQueue', () => { let concurrentCount = 0; let maxConcurrent = 0; - const processMessages = vi.fn(async (groupJid: string) => { + const processMessages = vi.fn(async (_groupJid: string) => { concurrentCount++; maxConcurrent = Math.max(maxConcurrent, concurrentCount); // Simulate async work @@ -69,7 +69,7 @@ describe('GroupQueue', () => { let maxActive = 0; const completionCallbacks: Array<() => void> = []; - const processMessages = vi.fn(async (groupJid: string) => { + const processMessages = vi.fn(async (_groupJid: string) => { activeCount++; maxActive = Math.max(maxActive, activeCount); await new Promise((resolve) => completionCallbacks.push(resolve)); @@ -104,7 +104,7 @@ describe('GroupQueue', () => { const executionOrder: string[] = []; let resolveFirst: () => void; - const processMessages = vi.fn(async (groupJid: string) => { + const processMessages = vi.fn(async (_groupJid: string) => { if (executionOrder.length === 0) { // First call: block until we release it await new Promise((resolve) => { diff --git a/src/group-queue.ts b/src/group-queue.ts index f2984ce..a3b547d 100644 --- a/src/group-queue.ts +++ b/src/group-queue.ts @@ -351,7 +351,7 @@ export class GroupQueue { // via idle timeout or container timeout. The --rm flag cleans them up on exit. // This prevents WhatsApp reconnection restarts from killing working agents. const activeContainers: string[] = []; - for (const [jid, state] of this.groups) { + for (const [_jid, state] of this.groups) { if (state.process && !state.process.killed && state.containerName) { activeContainers.push(state.containerName); } diff --git a/src/index.ts b/src/index.ts index 42329a0..db274f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,6 @@ import { getAllTasks, getMessagesSince, getNewMessages, - getRegisteredGroup, getRouterState, initDatabase, setRegisteredGroup, diff --git a/src/remote-control.test.ts b/src/remote-control.test.ts index 24e1b11..7dbf69c 100644 --- a/src/remote-control.test.ts +++ b/src/remote-control.test.ts @@ -37,7 +37,7 @@ describe('remote-control', () => { let readFileSyncSpy: ReturnType; let writeFileSyncSpy: ReturnType; let unlinkSyncSpy: ReturnType; - let mkdirSyncSpy: ReturnType; + let _mkdirSyncSpy: ReturnType; let openSyncSpy: ReturnType; let closeSyncSpy: ReturnType; @@ -50,7 +50,7 @@ describe('remote-control', () => { stdoutFileContent = ''; // Default fs mocks - mkdirSyncSpy = vi + _mkdirSyncSpy = vi .spyOn(fs, 'mkdirSync') .mockImplementation(() => undefined as any); writeFileSyncSpy = vi diff --git a/src/routing.test.ts b/src/routing.test.ts index 32bfc1f..6e44586 100644 --- a/src/routing.test.ts +++ b/src/routing.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import { _initTestDatabase, getAllChats, storeChatMetadata } from './db.js'; +import { _initTestDatabase, storeChatMetadata } from './db.js'; import { getAvailableGroups, _setRegisteredGroups } from './index.js'; beforeEach(() => { diff --git a/src/sender-allowlist.test.ts b/src/sender-allowlist.test.ts index 9e2513f..5bb8569 100644 --- a/src/sender-allowlist.test.ts +++ b/src/sender-allowlist.test.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { isSenderAllowed,