Skill: skl3/clawtk (version 1.1.0)

--- name: clawtk description: "Automatically reduce OpenClaw API costs by 60-80%. One-command setup: config optimization, spend caps, retry loop protection, and ClawTK Engine compression." version: 1.1.0 user-invocable: true command-dispatch: tool metadata: openclaw: emoji: "💰" homepage: https://clawtk.co requires: bins: [bash, jq, node] anyBins: [curl, wget] optionalBins: [sqlite3, brew] runtime: node os: [macos, linux] tags: [cost, optimization, tokens, budget, savings] --- # ClawTK — Cut Your OpenClaw Costs by 60-80% You are ClawTK, a cost optimization assistant for OpenClaw. You help users reduce their API spending through config optimization, spend caps, and token compression. ## First-Run Detection If the file `~/.openclaw/clawtk-state.json` does not exist, the user has not completed setup. Tell them: > ClawTK is installed but needs one-time setup. Say `/clawtk setup` to start. This will: > - Back up your current config > - Optimize heartbeat settings (biggest cost saver) > - Set context limits to prevent runaway spending > - Install spend caps to protect against retry loops > > Your original config is always restorable with `/clawtk restore`. ## Commands ### `/clawtk setup` Run the setup script to optimize the user's OpenClaw configuration: ```bash bash ~/.openclaw/skills/clawtk/scripts/setup.sh ``` After setup completes, summarize what changed and the estimated savings. ### `/clawtk status` Show current optimization status: ```bash bash ~/.openclaw/skills/clawtk/scripts/setup.sh --status ``` ### `/clawtk savings` Show actual cost savings since installation: ```bash bash ~/.openclaw/skills/clawtk/scripts/check-savings.sh ``` ### `/clawtk restore` Restore the original OpenClaw configuration from backup: ```bash bash ~/.openclaw/skills/clawtk/scripts/setup.sh --restore ``` Always confirm with the user before restoring. Warn them that this will remove all ClawTK optimizations. ### `/clawtk override` Temporarily disable spend caps for 1 hour (for when the user needs to do heavy work): ```bash bash ~/.openclaw/skills/clawtk/scripts/setup.sh --override ``` ### `/clawtk activate <key>` Activate ClawTK Pro with a license key: ```bash bash ~/.openclaw/skills/clawtk/scripts/activate-pro.sh "$1" ``` ### `/clawtk sync` Push local spend data to the ClawTK cloud dashboard (Pro/Cloud tier only): ```bash bash ~/.openclaw/skills/clawtk/scripts/sync.sh ``` Add `--compact` to also clean up old synced entries from the local log: ```bash bash ~/.openclaw/skills/clawtk/scripts/sync.sh --compact ``` If the user wants automatic syncing, suggest adding a cron job: `*/30 * * * * bash ~/.openclaw/skills/clawtk/scripts/sync.sh` ### `/clawtk uninstall` Completely remove ClawTK and restore original config: ```bash bash ~/.openclaw/skills/clawtk/scripts/uninstall.sh ``` Always confirm with the user before uninstalling. ## Runtime Requirements - **Node.js**: Required. Hook handlers (spend-guard, cache) are TypeScript and run in a Node-compatible runtime. - **sqlite3 CLI**: Optional. Required only for the semantic cache feature (Pro tier). If missing, caching is silently disabled — all other features work normally. - **brew**: Optional. Used as preferred install method for ClawTK Engine. Falls back to official installer if unavailable. ## Network Calls ClawTK makes network requests in exactly two scenarios, both requiring explicit user action: 1. **Engine install** (`/clawtk setup` with Pro tier, or `/clawtk activate`): Downloads the `rtk` binary via Homebrew or the official installer from `github.com/rtk-ai/rtk`. The installer URL is pinned to a tagged release. 2. **License validation & sync** (`/clawtk activate`, `/clawtk sync`): Contacts `api.clawtk.co` to validate license keys and (optionally) push spend data. Spend data contains only: timestamp, token count, estimated cost, and tool name. No message content, file contents, or conversation data is ever transmitted. No network calls are made during normal operation (spend-guard and cache hooks are fully local). ## Behavior Guidelines - When reporting savings, always show concrete dollar amounts, not just percentages - If the user complains about API costs, proactively suggest `/clawtk savings` to show their current optimization status - Never modify the user's config without explicit consent (setup requires the user to run `/clawtk setup`) - If a spend cap blocks an action, explain clearly what happened and how to override it

path: hooks/clawtk-cache/HOOK.md
content: ---
name: clawtk-cache
description: Semantic task caching for ClawTK Pro — skips redundant tool calls by returning cached results
event:
  - before_tool_call
  - tool_result_persist
priority: 50
---

Caches tool call results in a local SQLite database. When a semantically
identical tool call is detected, returns the cached result instead of
re-executing. Most effective for repeated file reads, test runs, and
project structure queries. Pro feature only.


path: hooks/clawtk-cache/handler.ts
content: import { readFileSync, writeFileSync, existsSync } from "fs";
import { join } from "path";
import { homedir } from "os";
import { execFileSync } from "child_process";

// ── Types ───────────────────────────────────────────────────────────────────

interface ClawTKState {
  tier: "free" | "pro" | "cloud";
}

interface ToolCallEvent {
  event: "before_tool_call" | "tool_result_persist";
  toolName: string;
  parameters: Record<string, unknown>;
  sessionId: string;
  result?: string;
}

interface HookResult {
  cached?: boolean;
  cachedResult?: string;
  skip?: boolean;
  message?: string;
}

// ── Config ──────────────────────────────────────────────────────────────────

const OPENCLAW_DIR = join(homedir(), ".openclaw");
const STATE_FILE = join(OPENCLAW_DIR, "clawtk-state.json");
const CACHE_DB = join(OPENCLAW_DIR, "clawtk-cache.db");

const MAX_CACHE_ENTRIES = 5000;

// Tools that are safe to cache (read-only, deterministic for a given state)
const CACHEABLE_TOOLS = new Set([
  "read_file",
  "list_directory",
  "search_files",
  "glob",
  "grep",
  "bash",
]);

// Bash commands safe to cache (read-only)
const CACHEABLE_BASH_PATTERNS = [
  /^(ls|tree|find|wc|du|df|cat|head|tail|file|stat)\b/,
  /^git\s+(status|log|diff|show|branch|tag|remote)\b/,
  /^(npm|yarn|pnpm)\s+(list|ls|outdated|info)\b/,
  /^(python|node|ruby|go)\s+--version\b/,
  /^(which|where|type|command\s+-v)\b/,
  /^rtk\b/,
];

// Bash commands that must NEVER be cached (side effects)
const UNCACHEABLE_BASH_PATTERNS = [
  /^(rm|mv|cp|mkdir|touch|chmod|chown)\b/,
  /^git\s+(add|commit|push|pull|checkout|merge|rebase|reset)\b/,
  /^(npm|yarn|pnpm)\s+(install|run|exec|publish)\b/,
  /\|/,
  />>?/,
  /&&/,
];

// ── Helpers ─────────────────────────────────────────────────────────────────

function loadState(): ClawTKState | null {
  if (!existsSync(STATE_FILE)) return null;
  try {
    return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
  } catch {
    return null;
  }
}

function isPro(state: ClawTKState): boolean {
  return state.tier === "pro" || state.tier === "cloud";
}

function hashKey(toolName: string, params: Record<string, unknown>): string {
  const key = `${toolName}:${JSON.stringify(params, Object.keys(params).sort())}`;
  let hash = 0x811c9dc5;
  for (let i = 0; i < key.length; i++) {
    hash ^= key.charCodeAt(i);
    hash = (hash * 0x01000193) | 0;
  }
  return (hash >>> 0).toString(36);
}

function isCacheableBashCommand(command: string): boolean {
  const trimmed = command.trim();
  for (const pattern of UNCACHEABLE_BASH_PATTERNS) {
    if (pattern.test(trimmed)) return false;
  }
  for (const pattern of CACHEABLE_BASH_PATTERNS) {
    if (pattern.test(trimmed)) return true;
  }
  return false;
}

function isCacheable(
  toolName: string,
  params: Record<string, unknown>
): boolean {
  if (!CACHEABLE_TOOLS.has(toolName)) return false;
  if (toolName === "bash") {
    const command = (params.command as string) || "";
    return isCacheableBashCommand(command);
  }
  return true;
}

// ── SQLite Operations ───────────────────────────────────────────────────────
// Uses sqlite3 CLI via execFileSync (no shell injection risk) to avoid
// native module dependencies. Keeps the hook zero-dependency.

function sqlite3(sql: string): string {
  try {
    return execFileSync("sqlite3", ["-json", CACHE_DB, sql], {
      encoding: "utf-8",
      stdio: ["pipe", "pipe", "pipe"],
      timeout: 5000,
    }).trim();
  } catch {
    return "";
  }
}

function sqlite3Exec(sql: string): void {
  try {
    execFileSync("sqlite3", [CACHE_DB, sql], {
      stdio: "pipe",
      timeout: 5000,
    });
  } catch {
    // Non-fatal
  }
}

function ensureDb(): boolean {
  if (!existsSync(CACHE_DB)) {
    try {
      sqlite3Exec(`
        CREATE TABLE IF NOT EXISTS cache (
          hash TEXT PRIMARY KEY,
          tool_name TEXT NOT NULL,
          result TEXT NOT NULL,
          hit_count INTEGER DEFAULT 1,
          created_at TEXT DEFAULT (datetime('now')),
          last_hit TEXT DEFAULT (datetime('now'))
        );
        CREATE INDEX IF NOT EXISTS idx_cache_tool ON cache(tool_name);
        CREATE INDEX IF NOT EXISTS idx_cache_created ON cache(created_at);
      `);
      return true;
    } catch {
      return false;
    }
  }
  return true;
}

function getCached(hash: string): string | null {
  const result = sqlite3(
    `SELECT result FROM cache WHERE hash = '${hash}' AND datetime(created_at) > datetime('now', '-4 hours') LIMIT 1`
  );
  if (!result || result === "[]") return null;

  try {
    const rows = JSON.parse(result);
    if (rows.length === 0) return null;

    // Bump hit count
    sqlite3Exec(
      `UPDATE cache SET hit_count = hit_count + 1, last_hit = datetime('now') WHERE hash = '${hash}'`
    );
    return rows[0].result;
  } catch {
    return null;
  }
}

function setCache(hash: string, toolName: string, result: string): void {
  // Write result to a temp file to avoid SQL injection from result content
  const tmpFile = join(OPENCLAW_DIR, `.clawtk-cache-tmp-${hash}`);
  try {
    writeFileSync(tmpFile, result, "utf-8");
    sqlite3Exec(
      `INSERT OR REPLACE INTO cache (hash, tool_name, result, hit_count, created_at, last_hit) VALUES ('${hash}', '${toolName}', readfile('${tmpFile}'), 1, datetime('now'), datetime('now'))`
    );
  } catch {
    // Non-fatal
  } finally {
    try {
      const { unlinkSync } = require("fs");
      unlinkSync(tmpFile);
    } catch {
      // cleanup failure is fine
    }
  }

  // Prune if over max
  sqlite3Exec(
    `DELETE FROM cache WHERE hash IN (SELECT hash FROM cache ORDER BY last_hit ASC LIMIT max(0, (SELECT count(*) FROM cache) - ${MAX_CACHE_ENTRIES}))`
  );
}

// ── Main Hook Handler ───────────────────────────────────────────────────────

export default function handler(event: ToolCallEvent): HookResult {
  const state = loadState();
  if (!state || !isPro(state)) return {};

  // Ensure sqlite3 CLI is available (declared as optionalBins in SKILL.md)
  try {
    execFileSync("which", ["sqlite3"], { stdio: "pipe" });
  } catch {
    // sqlite3 not installed — caching disabled, all other features work normally
    return {};
  }

  if (!ensureDb()) return {};

  const { toolName, parameters } = event;

  if (event.event === "before_tool_call") {
    if (!isCacheable(toolName, parameters)) return {};

    const hash = hashKey(toolName, parameters);
    const cached = getCached(hash);

    if (cached) {
      return {
        cached: true,
        cachedResult: cached,
        message: `[clawtk] Cache hit — saved ~${Math.round(cached.length / 4)} tokens`,
      };
    }
    return {};
  }

  if (event.event === "tool_result_persist") {
    if (!isCacheable(toolName, parameters)) return {};
    if (!event.result) return {};
    if (event.result.length > 50_000 || event.result.length < 10) return {};

    const hash = hashKey(toolName, parameters);
    setCache(hash, toolName, event.result);
    return {};
  }

  return {};
}


path: hooks/clawtk-spend-guard/HOOK.md
content: ---
name: clawtk-spend-guard
description: Budget enforcement and retry loop detection for ClawTK
event: before_tool_call
priority: 100
---

Enforces daily/weekly spend caps and detects retry loops that could burn
through API credits. Tracks spend in ~/.openclaw/clawtk-spend.jsonl.
Respects temporary overrides set via `/clawtk override`.


path: hooks/clawtk-spend-guard/handler.ts
content: import { readFileSync, appendFileSync, existsSync } from "fs";
import { join } from "path";
import { homedir } from "os";

// ── Types ───────────────────────────────────────────────────────────────────

interface ClawTKState {
  tier: "free" | "pro" | "cloud";
  spendCaps: {
    daily: number;
    weekly: number;
  };
  overrideUntil: string | null;
}

interface SpendEntry {
  timestamp: string;
  tokens: number;
  estimatedCost: number;
  toolName: string;
  toolCallHash: string;
}

interface ToolCallEvent {
  toolName: string;
  parameters: Record<string, unknown>;
  sessionId: string;
  tokensSoFar?: number;
}

interface HookResult {
  block?: boolean;
  blockReason?: string;
  warn?: boolean;
  message?: string;
}

// ── Config ──────────────────────────────────────────────────────────────────

const OPENCLAW_DIR = join(homedir(), ".openclaw");
const STATE_FILE = join(OPENCLAW_DIR, "clawtk-state.json");
const SPEND_LOG = join(OPENCLAW_DIR, "clawtk-spend.jsonl");

// Retry loop detection: same tool call pattern N times within this window
const RETRY_LOOP_THRESHOLD = 3;
const RETRY_LOOP_WINDOW_MS = 60_000;

// Cost estimation: rough tokens-per-tool-call average
// Actual costs depend on model, but this gives a ballpark for cap enforcement
const AVG_TOKENS_PER_TOOL_CALL = 2000;
const COST_PER_1K_TOKENS = 0.003; // ~$3/M tokens blended average

// ── Helpers ─────────────────────────────────────────────────────────────────

function loadState(): ClawTKState | null {
  if (!existsSync(STATE_FILE)) return null;
  try {
    return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
  } catch {
    return null;
  }
}

function hashToolCall(event: ToolCallEvent): string {
  const key = `${event.toolName}:${JSON.stringify(event.parameters)}`;
  let hash = 0;
  for (let i = 0; i < key.length; i++) {
    const char = key.charCodeAt(i);
    hash = ((hash << 5) - hash + char) | 0;
  }
  return hash.toString(36);
}

function loadRecentSpend(): SpendEntry[] {
  if (!existsSync(SPEND_LOG)) return [];

  try {
    const lines = readFileSync(SPEND_LOG, "utf-8").trim().split("\n");
    const now = Date.now();
    const weekAgo = now - 7 * 24 * 60 * 60 * 1000;

    return lines
      .filter((line) => line.trim())
      .map((line) => {
        try {
          return JSON.parse(line) as SpendEntry;
        } catch {
          return null;
        }
      })
      .filter((entry): entry is SpendEntry => {
        if (!entry) return false;
        return new Date(entry.timestamp).getTime() > weekAgo;
      });
  } catch {
    return [];
  }
}

function logSpend(entry: SpendEntry): void {
  appendFileSync(SPEND_LOG, JSON.stringify(entry) + "\n");
}

function isOverrideActive(state: ClawTKState): boolean {
  if (!state.overrideUntil) return false;
  return new Date(state.overrideUntil).getTime() > Date.now();
}

// ── Retry Loop Detection ────────────────────────────────────────────────────

function detectRetryLoop(
  entries: SpendEntry[],
  currentHash: string
): boolean {
  const now = Date.now();
  const recentSame = entries.filter(
    (e) =>
      e.toolCallHash === currentHash &&
      now - new Date(e.timestamp).getTime() < RETRY_LOOP_WINDOW_MS
  );
  return recentSame.length >= RETRY_LOOP_THRESHOLD;
}

// ── Spend Cap Check ─────────────────────────────────────────────────────────

function checkSpendCaps(
  entries: SpendEntry[],
  caps: ClawTKState["spendCaps"]
): { exceeded: boolean; level: "ok" | "warning" | "exceeded"; detail: string } {
  const now = Date.now();
  const dayAgo = now - 24 * 60 * 60 * 1000;
  const weekAgo = now - 7 * 24 * 60 * 60 * 1000;

  const dailySpend = entries
    .filter((e) => new Date(e.timestamp).getTime() > dayAgo)
    .reduce((sum, e) => sum + e.estimatedCost, 0);

  const weeklySpend = entries
    .filter((e) => new Date(e.timestamp).getTime() > weekAgo)
    .reduce((sum, e) => sum + e.estimatedCost, 0);

  if (dailySpend >= caps.daily) {
    return {
      exceeded: true,
      level: "exceeded",
      detail: `Daily spend cap reached: $${dailySpend.toFixed(2)} / $${caps.daily} limit. Run /clawtk override for a 1-hour bypass.`,
    };
  }

  if (weeklySpend >= caps.weekly) {
    return {
      exceeded: true,
      level: "exceeded",
      detail: `Weekly spend cap reached: $${weeklySpend.toFixed(2)} / $${caps.weekly} limit. Run /clawtk override for a 1-hour bypass.`,
    };
  }

  if (dailySpend >= caps.daily * 0.8) {
    return {
      exceeded: false,
      level: "warning",
      detail: `Approaching daily cap: $${dailySpend.toFixed(2)} / $${caps.daily} (${Math.round((dailySpend / caps.daily) * 100)}%)`,
    };
  }

  return { exceeded: false, level: "ok", detail: "" };
}

// ── Main Hook Handler ───────────────────────────────────────────────────────

export default function handler(event: ToolCallEvent): HookResult {
  const state = loadState();
  if (!state) return {}; // ClawTK not set up, pass through

  const callHash = hashToolCall(event);
  const entries = loadRecentSpend();

  // Check for retry loops (always active, even during override)
  if (detectRetryLoop(entries, callHash)) {
    return {
      block: true,
      blockReason: `ClawTK: Retry loop detected — "${event.toolName}" called ${RETRY_LOOP_THRESHOLD}+ times in ${RETRY_LOOP_WINDOW_MS / 1000}s. This pattern can burn hundreds of dollars. The loop has been stopped. If this was intentional, run /clawtk override to continue.`,
    };
  }

  // Log this tool call
  const estimatedCost =
    (AVG_TOKENS_PER_TOOL_CALL / 1000) * COST_PER_1K_TOKENS;

  logSpend({
    timestamp: new Date().toISOString(),
    tokens: AVG_TOKENS_PER_TOOL_CALL,
    estimatedCost,
    toolName: event.toolName,
    toolCallHash: callHash,
  });

  // Skip cap check if override is active
  if (isOverrideActive(state)) return {};

  // Check spend caps
  const capStatus = checkSpendCaps(entries, state.spendCaps);

  if (capStatus.exceeded) {
    return {
      block: true,
      blockReason: `ClawTK: ${capStatus.detail}`,
    };
  }

  if (capStatus.level === "warning") {
    return {
      warn: true,
      message: `ClawTK: ${capStatus.detail}`,
    };
  }

  return {};
}


path: package.json
content: {
  "name": "clawtk",
  "version": "1.1.0",
  "description": "Automatically reduce OpenClaw API costs by 60-80%",
  "homepage": "https://clawtk.co",
  "license": "MIT-0",
  "openclaw": {
    "hooks": {
      "clawtk-spend-guard": {
        "path": "./hooks/clawtk-spend-guard",
        "event": "before_tool_call"
      },
      "clawtk-cache": {
        "path": "./hooks/clawtk-cache",
        "event": ["before_tool_call", "tool_result_persist"]
      }
    }
  }
}


path: references/config-patches.md
content: # ClawTK Config Patches

Every change ClawTK makes to your OpenClaw config, explained.

## Heartbeat Isolation (`isolatedSession: true`, `lightContext: true`)

**What it does**: Runs heartbeat checks in a separate, minimal session instead of your main session.

**Why it saves money**: A default heartbeat sends your entire session context (~100K tokens) with every check. With isolation, heartbeats only send ~3K tokens. If your heartbeat fires every 30 minutes, that's 100K wasted tokens every half hour.

**Savings**: ~97% reduction per heartbeat call.

## Heartbeat Interval (`every: "55m"`)

**What it does**: Changes how often OpenClaw checks for new messages/events.

**Why 55 minutes**: Anthropic's prompt cache has a 60-minute TTL. By firing at 55 minutes, we keep the cache warm so the next real interaction gets the 90% cache discount. The default 30-minute interval means nearly double the heartbeat calls.

**Savings**: ~45% fewer heartbeat API calls.

## Heartbeat Model (`model: "google/gemini-2.5-flash-lite"`)

**What it does**: Uses Google's cheapest model for heartbeat checks instead of your primary model.

**Why it's safe**: Heartbeats are simple "has anything changed?" checks. They don't need Opus-level reasoning. Flash Lite costs ~98% less per token.

**Savings**: ~98% cost reduction per heartbeat.

## Context Token Cap (`contextTokens: 100000`)

**What it does**: Limits the context window to 100K tokens instead of the default 200K.

**Why it saves money**: Larger context = more tokens billed per API call. Most tasks work fine within 100K. The default 200K causes unnecessary context accumulation.

**Savings**: Prevents runaway costs from context bloat.

## Image Downscaling (`imageMaxDimensionPx: 800`)

**What it does**: Resizes images to max 800px before sending to the model (default is 1200px).

**Why it's safe**: 800px captures all readable text and UI detail. The extra resolution at 1200px rarely adds useful information but costs ~40% more in vision tokens.

**Savings**: ~40% fewer vision tokens.

## Compaction Model (`compaction.model: "google/gemini-2.5-flash-lite"`)

**What it does**: Uses a cheap model for context compaction (summarizing old messages to free up context space).

**Why it's safe**: Compaction is summarization, not complex reasoning. A cheaper model does this equally well.

**Savings**: Reduces compaction costs to near-zero.

---

## Restoring Your Config

All patches are fully reversible. Run `/clawtk restore` to revert to your original config (backed up before any changes).


path: references/savings-guide.md
content: # ClawTK Savings Guide

## Where Your Money Goes (Typical OpenClaw User)

| Cost Center | % of Bill | What Causes It |
|-------------|-----------|----------------|
| Heartbeats | 30-50% | Background checks sending full context every 30min |
| Session context | 20-30% | Messages + tool outputs accumulating in session |
| Tool outputs | 15-25% | Large CLI outputs (git log, test results, ls) stored in context |
| Model choice | 5-10% | Using Opus for simple tasks |
| Images | 3-5% | Screenshots at full resolution |

## How ClawTK Reduces Each Cost

### Free Tier (30-50% savings)

1. **Heartbeat isolation** cuts the biggest cost center by ~97%
2. **Heartbeat interval** reduces frequency by ~45%
3. **Cheap heartbeat model** reduces per-heartbeat cost by ~98%
4. **Context cap** prevents runaway accumulation
5. **Spend caps** prevent catastrophic overnight burns
6. **Retry loop detection** catches the "$47 overnight" problem

### Pro Tier (60-80% savings)

Everything in Free, plus:

7. **ClawTK Engine compression** reduces CLI output tokens by 60-90% — the single biggest lever for active users
8. **Semantic caching** skips the LLM entirely for repeated tasks
9. **Custom spend caps** tuned to your usage patterns

## Real Numbers

**Before ClawTK** (typical active user):
- Heartbeat cost: ~$3/hr (100K tokens x $0.003/1K x 10 heartbeats/day)
- Session cost: $15-30/day
- Monthly: $150-300

**After ClawTK Free**:
- Heartbeat cost: ~$0.03/hr (97% reduction)
- Session cost: $10-20/day
- Monthly: $50-100

**After ClawTK Pro**:
- Heartbeat cost: ~$0.03/hr
- Session cost: $5-10/day (ClawTK Engine compression)
- Monthly: $30-60


path: references/troubleshooting.md
content: # ClawTK Troubleshooting

## Setup Issues

### "jq is required but not installed"

Install jq:
- macOS: `brew install jq`
- Ubuntu/Debian: `sudo apt install jq`
- Other Linux: check your package manager or download from https://jqlang.github.io/jq/

### "OpenClaw config not found"

Make sure OpenClaw is installed and has been run at least once. The config file should be at `~/.openclaw/openclaw.json`. Run `openclaw doctor` to check your installation.

### Setup completed but changes don't seem active

Restart your OpenClaw gateway after setup:
```
openclaw gateway restart
```

## Spend Guard Issues

### "Daily spend cap reached" blocking my work

Two options:
1. **Temporary bypass**: Say `/clawtk override` for a 1-hour override
2. **Permanent change** (Pro only): Edit `~/.openclaw/clawtk-state.json` and change `spendCaps.daily` to a higher value

### "Retry loop detected" but I'm not in a loop

The retry loop detector triggers when the same tool call runs 3+ times in 60 seconds. If you're intentionally running the same command repeatedly, use `/clawtk override` for a temporary bypass.

### Spend tracking seems inaccurate

ClawTK estimates costs based on average token consumption (~2000 tokens per tool call at ~$0.003/1K tokens). Actual costs vary by model and prompt size. For precise tracking, check your API provider's usage dashboard.

## ClawTK Engine Issues (Pro)

### Engine not found after setup

Check if ClawTK Engine is in your PATH:
```
which rtk
rtk --version
```

If not found:
- Homebrew users: `brew install rtk`
- Others: `curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh`

Then add `~/.local/bin` to your PATH if needed.

### Engine hook not activating

Run `rtk init -g --auto-patch` to reinstall the hook. If using sandbox mode, ensure ClawTK Engine binary is in the sandbox's PATH.

### Engine compressing output I need to see in full

ClawTK Engine compresses output before the LLM sees it, but the full output is still available in your terminal. If you need the LLM to see full output for a specific command, prefix it with `command` to bypass the Engine:
```
command git log --oneline -20
```

## Uninstall

To completely remove ClawTK and restore your original config:
```
/clawtk uninstall
```

This will:
1. Restore your original OpenClaw config from backup
2. Unregister all hooks
3. Remove all ClawTK files (state, spend log, cache)
4. Remove the skill directory

Restart your gateway afterward: `openclaw gateway restart`


path: scripts/activate-pro.sh
content: #!/usr/bin/env bash
# ClawTK Pro Activation
# Validates a license key and unlocks Pro features.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OPENCLAW_DIR="${OPENCLAW_DIR:-$HOME/.openclaw}"
STATE_FILE="$OPENCLAW_DIR/clawtk-state.json"
API_BASE="https://api.clawtk.co/v1"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'

log()  { echo -e "${GREEN}[clawtk]${NC} $1"; }
warn() { echo -e "${YELLOW}[clawtk]${NC} $1"; }
err()  { echo -e "${RED}[clawtk]${NC} $1" >&2; }

# ── Validate Key Format ─────────────────────────────────────────────────────

validate_format() {
    local key="$1"
    # Expected format: XXXX-XXXX-XXXX or XXXX-XXXX-XXXX-XXXX
    if [[ ! "$key" =~ ^[A-Z0-9]{4}(-[A-Z0-9]{4}){2,3}$ ]]; then
        err "Invalid key format: $key"
        err "Expected format: XXXX-XXXX-XXXX"
        exit 1
    fi
}

# ── Validate Against API ────────────────────────────────────────────────────

validate_key() {
    local key="$1"
    local response

    log "Validating license key..."

    if command -v curl &>/dev/null; then
        response=$(curl -sS --max-time 10 \
            -H "Content-Type: application/json" \
            -d "{\"key\": \"$key\"}" \
            "$API_BASE/validate" 2>&1) || {
            err "Could not reach activation server."
            err "Check your internet connection and try again."
            err "If the problem persists, contact [REDACTED_SECRET]"
            exit 1
        }
    elif command -v wget &>/dev/null; then
        response=$(wget -qO- --timeout=10 \
            --header="Content-Type: application/json" \
            --post-data="{\"key\": \"$key\"}" \
            "$API_BASE/validate" 2>&1) || {
            err "Could not reach activation server."
            exit 1
        }
    else
        err "Neither curl nor wget found."
        exit 1
    fi

    # Parse response
    local valid tier
    valid=$(echo "$response" | jq -r '.valid // false' 2>/dev/null)
    tier=$(echo "$response" | jq -r '.tier // "pro"' 2>/dev/null)

    if [ "$valid" != "true" ]; then
        local message
        message=$(echo "$response" | jq -r '.message // "Invalid or expired license key."' 2>/dev/null)
        err "$message"
        err ""
        err "Purchase a key at https://clawtk.co/pro"
        exit 1
    fi

    echo "$tier"
}

# ── Update State ─────────────────────────────────────────────────────────────

activate_state() {
    local tier="$1"
    local key="$2"

    if [ ! -f "$STATE_FILE" ]; then
        err "ClawTK is not set up. Run /clawtk setup first."
        exit 1
    fi

    local updated
    updated=$(jq \
        --arg tier "$tier" \
        --arg key "$key" \
        --arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
        '.tier = $tier | .licenseKey = $key | .activatedAt = $date' \
        "$STATE_FILE")
    echo "$updated" > "$STATE_FILE"
}

# ── Install Pro Features ────────────────────────────────────────────────────

install_pro_features() {
    echo ""
    log "Installing Pro features..."

    # Install ClawTK Engine
    log "Setting up ClawTK Engine token compression..."
    bash "$SCRIPT_DIR/install-engine.sh" install || {
        warn "Engine installation failed. You can install it manually later."
        warn "Run: /clawtk setup (it will retry Engine installation)"
    }

    echo ""
    echo -e "${BOLD}${GREEN}Pro activated!${NC}"
    echo ""
    echo "New features unlocked:"
    echo "  [x] ClawTK Engine token compression (60-90% savings on CLI output)"
    echo "  [x] Custom spend caps (edit via /clawtk config)"
    echo "  [x] Semantic task caching"
    echo ""
    echo "Run /clawtk savings after your next session to see the difference."
}

# ── Main ─────────────────────────────────────────────────────────────────────

main() {
    local key="${1:-}"

    if [ -z "$key" ]; then
        err "Usage: activate-pro.sh <license-key>"
        err "Example: activate-pro.sh ABCD-1234-EFGH"
        err ""
        err "Purchase a key at https://clawtk.co/pro"
        exit 1
    fi

    # Normalize to uppercase
    key=$(echo "$key" | tr '[:lower:]' '[:upper:]')

    validate_format "$key"

    local tier
    tier=$(validate_key "$key")

    activate_state "$tier" "$key"
    log "License validated. Tier: $(echo "$tier" | tr '[:lower:]' '[:upper:]')"

    install_pro_features
}

main "$@"


path: scripts/check-savings.sh
content: #!/usr/bin/env bash
# ClawTK Savings Report
# Parses spend log and Engine gain data to show actual cost savings.

set -euo pipefail

OPENCLAW_DIR="${OPENCLAW_DIR:-$HOME/.openclaw}"
STATE_FILE="$OPENCLAW_DIR/clawtk-state.json"
SPEND_LOG="$OPENCLAW_DIR/clawtk-spend.jsonl"

# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'

if [ ! -f "$STATE_FILE" ]; then
    echo "ClawTK is not set up yet. Run /clawtk setup first."
    exit 1
fi

tier=$(jq -r '.tier' "$STATE_FILE")
install_date=$(jq -r '.installDate' "$STATE_FILE")

echo -e "${BOLD}ClawTK Savings Report${NC}"
echo -e "${CYAN}════════════════════════════════════════${NC}"
echo ""

# ── Spend Log Analysis ───────────────────────────────────────────────────────

if [ -f "$SPEND_LOG" ]; then
    now=$(date +%s)
    day_ago=$((now - 86400))
    week_ago=$((now - 604800))

    # Parse spend log with jq
    total_entries=$(wc -l < "$SPEND_LOG" | tr -d ' ')

    daily_spend=$(jq -s --argjson cutoff "$day_ago" '
        [.[] | select((.timestamp | fromdateiso8601) > $cutoff) | .estimatedCost] | add // 0
    ' "$SPEND_LOG")

    weekly_spend=$(jq -s --argjson cutoff "$week_ago" '
        [.[] | select((.timestamp | fromdateiso8601) > $cutoff) | .estimatedCost] | add // 0
    ' "$SPEND_LOG")

    total_spend=$(jq -s '[.[].estimatedCost] | add // 0' "$SPEND_LOG")

    daily_tokens=$(jq -s --argjson cutoff "$day_ago" '
        [.[] | select((.timestamp | fromdateiso8601) > $cutoff) | .tokens] | add // 0
    ' "$SPEND_LOG")

    weekly_tokens=$(jq -s --argjson cutoff "$week_ago" '
        [.[] | select((.timestamp | fromdateiso8601) > $cutoff) | .tokens] | add // 0
    ' "$SPEND_LOG")

    # Blocked calls (retry loops caught)
    loops_blocked=$(jq -s '[.[] | select(.toolName == "__blocked_retry_loop")] | length' "$SPEND_LOG" 2>/dev/null || echo "0")

    echo "Spend Tracking (since $install_date)"
    echo -e "${CYAN}────────────────────────────────────────${NC}"
    printf "  %-24s %s\n" "Today:" "\$$(printf '%.2f' "$daily_spend") ($((daily_tokens / 1000))K tokens)"
    printf "  %-24s %s\n" "This week:" "\$$(printf '%.2f' "$weekly_spend") ($((weekly_tokens / 1000))K tokens)"
    printf "  %-24s %s\n" "All time:" "\$$(printf '%.2f' "$total_spend")"
    printf "  %-24s %s\n" "Tool calls tracked:" "$total_entries"
    printf "  %-24s %s\n" "Retry loops caught:" "$loops_blocked"
    echo ""
else
    echo "  No spend data yet. Data will appear after your next session."
    echo ""
fi

# ── Config Savings Estimate ──────────────────────────────────────────────────

echo "Estimated Savings from Config Optimizations"
echo -e "${CYAN}────────────────────────────────────────${NC}"
echo ""
echo "  Based on typical OpenClaw usage ($150/month baseline):"
echo ""
printf "  %-30s ${GREEN}%s${NC}\n" "Heartbeat isolation:" "~\$45-65/mo saved"
printf "  %-30s ${GREEN}%s${NC}\n" "Heartbeat interval (55m):" "~\$10-15/mo saved"
printf "  %-30s ${GREEN}%s${NC}\n" "Cheap heartbeat model:" "~\$15-25/mo saved"
printf "  %-30s ${GREEN}%s${NC}\n" "Context cap (100K):" "~\$5-15/mo saved"
printf "  %-30s ${GREEN}%s${NC}\n" "Image downscaling:" "~\$3-5/mo saved"
echo ""
echo -e "  ${BOLD}Estimated total: \$78-125/mo saved (52-83%)${NC}"
echo ""

# ── Engine Savings ──────────────────────────────────────────────────────────────

if command -v rtk &>/dev/null; then
    echo "ClawTK Engine Compression"
    echo -e "${CYAN}────────────────────────────────────────${NC}"
    rtk gain 2>/dev/null || echo "  Run some commands first to see Engine savings."
    echo ""
else
    if [ "$tier" = "free" ]; then
        echo -e "${YELLOW}ClawTK Engine Compression (Pro only)${NC}"
        echo -e "${CYAN}────────────────────────────────────────${NC}"
        echo "  ClawTK Engine compresses CLI output by 60-90% before it reaches the LLM."
        echo "  This is the single biggest cost saver for heavy users."
        echo ""
        echo "  Upgrade: https://clawtk.co/pro (\$49 one-time)"
        echo ""
    fi
fi

# ── Spend Caps Status ────────────────────────────────────────────────────────

daily_cap=$(jq -r '.spendCaps.daily' "$STATE_FILE")
weekly_cap=$(jq -r '.spendCaps.weekly' "$STATE_FILE")
override=$(jq -r '.overrideUntil // "none"' "$STATE_FILE")

echo "Spend Caps"
echo -e "${CYAN}────────────────────────────────────────${NC}"
printf "  %-20s \$%s/day\n" "Daily cap:" "$daily_cap"
printf "  %-20s \$%s/week\n" "Weekly cap:" "$weekly_cap"
printf "  %-20s %s\n" "Override:" "$override"
echo ""
echo -e "${CYAN}════════════════════════════════════════${NC}"


path: scripts/install-engine.sh
content: #!/usr/bin/env bash
# ClawTK ClawTK Engine Installer
# Downloads and installs the ClawTK Engine binary for token compression.
# Tries Homebrew first, falls back to official curl installer.
# Verifies binary after install.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
OPENCLAW_DIR="${OPENCLAW_DIR:-$HOME/.openclaw}"
STATE_FILE="$OPENCLAW_DIR/clawtk-state.json"
RTK_VERSION_FILE="$SKILL_DIR/templates/rtk-version.txt"
RTK_MIN_VERSION="0.28.0"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

log()  { echo -e "${GREEN}[clawtk]${NC} $1"; }
warn() { echo -e "${YELLOW}[clawtk]${NC} $1"; }
err()  { echo -e "${RED}[clawtk]${NC} $1" >&2; }

# ── Version Check ────────────────────────────────────────────────────────────

# Compare semver: returns 0 if $1 >= $2
version_gte() {
    local IFS=.
    local i ver1=($1) ver2=($2)
    for ((i = 0; i < ${#ver2[@]}; i++)); do
        local v1="${ver1[i]:-0}"
        local v2="${ver2[i]:-0}"
        if ((v1 > v2)); then return 0; fi
        if ((v1 < v2)); then return 1; fi
    done
    return 0
}

get_rtk_version() {
    rtk --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1
}

# ── Detect Platform ──────────────────────────────────────────────────────────

detect_platform() {
    local os arch

    case "$(uname -s)" in
        Darwin) os="macos" ;;
        Linux)  os="linux" ;;
        *)
            err "Unsupported OS: $(uname -s)"
            err "Engine supports macOS and Linux."
            exit 1
            ;;
    esac

    case "$(uname -m)" in
        x86_64|amd64)  arch="x86_64" ;;
        arm64|aarch64) arch="arm64" ;;
        *)
            err "Unsupported architecture: $(uname -m)"
            exit 1
            ;;
    esac

    echo "${os}_${arch}"
}

# ── Install via Homebrew ─────────────────────────────────────────────────────

install_brew() {
    if ! command -v brew &>/dev/null; then
        return 1
    fi

    log "Installing Engine via Homebrew..."
    brew install rtk

    if command -v rtk &>/dev/null; then
        log "Engine installed via Homebrew."
        return 0
    fi

    warn "Homebrew install completed but rtk not found in PATH."
    return 1
}

# ── Install via Official Script ──────────────────────────────────────────────

install_curl() {
    local rtk_version
    rtk_version=$(cat "$RTK_VERSION_FILE" 2>/dev/null | tr -d '[:space:]')
    rtk_version="${rtk_version:-$RTK_MIN_VERSION}"

    local installer_url="https://raw.githubusercontent.com/rtk-ai/rtk/refs/tags/v${rtk_version}/install.sh"

    warn "Engine is not available via Homebrew on this system."
    warn "The installer will download and run a shell script from:"
    warn "  $installer_url"
    warn ""
    warn "If you prefer, you can install manually instead:"
    warn "  cargo install rtk@$rtk_version"
    warn "  # or download from: https://github.com/rtk-ai/rtk/releases/tag/v${rtk_version}"
    warn ""

    log "Installing Engine v$rtk_version via official installer..."

    local tmp_installer
    tmp_installer=$(mktemp "${TMPDIR:-/tmp}/rtk-install-XXXXXX.sh")

    if command -v curl &>/dev/null; then
        curl -fsSL "$installer_url" -o "$tmp_installer"
    elif command -v wget &>/dev/null; then
        wget -qO "$tmp_installer" "$installer_url"
    else
        err "Neither curl nor wget found. Cannot download ClawTK Engine."
        rm -f "$tmp_installer"
        exit 1
    fi

    # Show what will be executed so the user/reviewer can inspect
    log "Installer downloaded to $tmp_installer ($(wc -c < "$tmp_installer" | tr -d ' ') bytes)"

    sh "$tmp_installer"
    rm -f "$tmp_installer"

    # The official installer puts the binary in ~/.local/bin
    local local_bin="$HOME/.local/bin"

    if [ -f "$local_bin/rtk" ] && ! command -v rtk &>/dev/null; then
        warn "Engine installed to $local_bin but it's not in your PATH."
        warn "Add this to your shell profile (~/.zshrc or ~/.bashrc):"
        warn '  export PATH="$HOME/.local/bin:$PATH"'
        warn ""
        warn "Then restart your terminal or run: source ~/.zshrc"

        # Temporarily add to PATH for this session
        export PATH="$local_bin:$PATH"
    fi

    if command -v rtk &>/dev/null; then
        log "Engine installed via official installer."
        return 0
    fi

    err "Engine installation failed."
    return 1
}

# ── Configure ClawTK Engine for OpenClaw ───────────────────────────────────────────────

configure_rtk() {
    log "Configuring Engine for OpenClaw..."

    # Initialize Engine globally with auto-patch (no prompts)
    # This installs the PreToolUse hook that transparently rewrites bash commands
    rtk init -g --auto-patch 2>/dev/null || {
        warn "Could not auto-configure Engine hooks."
        warn "Run manually: rtk init -g"
        return 1
    }

    log "Engine hooks configured. Bash commands will be compressed automatically."
}

# ── Update State File ────────────────────────────────────────────────────────

update_state() {
    local version="$1"

    if [ ! -f "$STATE_FILE" ]; then
        warn "ClawTK state file not found. Run /clawtk setup first."
        return
    fi

    local updated
    updated=$(jq \
        --arg ver "$version" \
        '.rtkInstalled = true | .rtkVersion = $ver' \
        "$STATE_FILE")
    echo "$updated" > "$STATE_FILE"
}

# ── Main ─────────────────────────────────────────────────────────────────────

main() {
    local action="${1:-install}"

    case "$action" in
        install)
            # Check if already installed and sufficient version
            if command -v rtk &>/dev/null; then
                local current_version
                current_version=$(get_rtk_version)

                if [ -n "$current_version" ]; then
                    if version_gte "$current_version" "$RTK_MIN_VERSION"; then
                        log "Engine v$current_version already installed (meets minimum v$RTK_MIN_VERSION)."
                        configure_rtk
                        update_state "$current_version"
                        return 0
                    else
                        warn "Engine v$current_version installed but v$RTK_MIN_VERSION+ required. Upgrading..."
                    fi
                fi
            fi

            local platform
            platform=$(detect_platform)
            log "Detected platform: $platform"

            # Try Homebrew first, fall back to curl installer
            if install_brew; then
                true  # success
            elif install_curl; then
                true  # success
            else
                err "Failed to install Engine."
                err ""
                err "Manual install options:"
                err "  Homebrew:  brew install rtk"
                err "  Cargo:    cargo install rtk"
                err "  Binary:   https://github.com/rtk-ai/rtk/releases"
                exit 1
            fi

            # Verify
            local installed_version
            installed_version=$(get_rtk_version)

            if [ -z "$installed_version" ]; then
                err "Engine installed but version check failed."
                exit 1
            fi

            log "Engine v$installed_version installed successfully."

            # Configure hooks
            configure_rtk

            # Update state
            update_state "$installed_version"

            echo ""
            log "Engine is ready. CLI output will be compressed by 60-90%."
            log "Run 'rtk gain' after a session to see your token savings."
            ;;

        check)
            # Version check only (used by /clawtk status)
            if ! command -v rtk &>/dev/null; then
                echo "not_installed"
                return 1
            fi

            local version
            version=$(get_rtk_version)

            if [ -z "$version" ]; then
                echo "unknown"
                return 1
            fi

            if version_gte "$version" "$RTK_MIN_VERSION"; then
                echo "$version"
                return 0
            else
                echo "outdated:$version"
                return 1
            fi
            ;;

        *)
            err "Unknown action: $action"
            echo "Usage: install-engine.sh [install|check]"
            exit 1
            ;;
    esac
}

main "$@"


path: scripts/patch-config.sh
content: #!/usr/bin/env bash
# ClawTK Config Patcher
# Optimizes OpenClaw config for cost reduction via jq transforms.
# Always backs up before modifying. All changes are additive (set-if-absent
# for user-customized fields, override for defaults).

set -euo pipefail

OPENCLAW_DIR="${OPENCLAW_DIR:-$HOME/.openclaw}"
CONFIG_FILE="$OPENCLAW_DIR/openclaw.json"
STATE_FILE="$OPENCLAW_DIR/clawtk-state.json"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

log()  { echo -e "${GREEN}[clawtk]${NC} $1"; }
warn() { echo -e "${YELLOW}[clawtk]${NC} $1"; }
err()  { echo -e "${RED}[clawtk]${NC} $1" >&2; }

# Check prerequisites
check_prereqs() {
    if ! command -v jq &>/dev/null; then
        err "jq is required but not installed."
        err "Install it: brew install jq (macOS) or apt install jq (Linux)"
        exit 1
    fi

    if [ ! -f "$CONFIG_FILE" ]; then
        err "OpenClaw config not found at $CONFIG_FILE"
        err "Is OpenClaw installed? Run: openclaw doctor"
        exit 1
    fi
}

# Back up current config
backup_config() {
    local timestamp
    timestamp=$(date +%Y%m%d_%H%M%S)
    local backup_path="$CONFIG_FILE.clawtk-backup.$timestamp"

    cp "$CONFIG_FILE" "$backup_path"
    log "Config backed up to: $backup_path"
    echo "$backup_path"
}

# Apply cost-saving patches to config
patch_config() {
    local backup_path="$1"

    log "Applying cost optimizations..."

    # Build jq filter that applies all patches
    # Each patch uses //= (alternative assignment) to preserve user overrides
    # where appropriate, and direct assignment for critical optimizations
    local jq_filter='
    # Heartbeat isolation — biggest single saver
    # Runs heartbeats in a separate lightweight session (~3K tokens instead of ~100K)
    .agents.defaults.heartbeat.isolatedSession = true |
    .agents.defaults.heartbeat.lightContext = true |

    # Heartbeat interval — 55min keeps Anthropic prompt cache warm (60min TTL)
    # while reducing heartbeat frequency by ~45% vs default 30min
    .agents.defaults.heartbeat.every = "55m" |

    # Heartbeat model — use cheapest available for background checks
    # Heartbeats are simple "anything new?" checks, no reasoning needed
    .agents.defaults.heartbeat.model = "google/gemini-2.5-flash-lite" |

    # Context token cap — prevent runaway context accumulation
    # 100K is plenty for most tasks; default 200K wastes tokens on stale context
    .agents.defaults.contextTokens = 100000 |

    # Image downscaling — 800px captures all detail needed for screenshots
    # Default 1200px wastes ~40% more vision tokens
    .agents.defaults.imageMaxDimensionPx = 800 |

    # Compaction model — use cheaper model for context compaction
    .agents.defaults.compaction.model = "google/gemini-2.5-flash-lite"
    '

    # Apply patches
    local patched
    patched=$(jq "$jq_filter" "$CONFIG_FILE")

    if [ $? -ne 0 ]; then
        err "Failed to apply patches. Config unchanged."
        err "Backup is at: $backup_path"
        exit 1
    fi

    echo "$patched" > "$CONFIG_FILE"
    log "Config patched successfully."
}

# Show diff of what changed
show_diff() {
    local backup_path="$1"

    echo ""
    log "Changes applied:"
    echo -e "${CYAN}────────────────────────────────────────${NC}"

    # Show human-readable diff of key fields
    local old_heartbeat_interval new_heartbeat_interval
    old_heartbeat_interval=$(jq -r '.agents.defaults.heartbeat.every // "30m (default)"' "$backup_path" 2>/dev/null || echo "30m (default)")
    new_heartbeat_interval=$(jq -r '.agents.defaults.heartbeat.every // "not set"' "$CONFIG_FILE")

    local old_isolated new_isolated
    old_isolated=$(jq -r '.agents.defaults.heartbeat.isolatedSession // false' "$backup_path" 2>/dev/null || echo "false")
    new_isolated=$(jq -r '.agents.defaults.heartbeat.isolatedSession // false' "$CONFIG_FILE")

    local old_context new_context
    old_context=$(jq -r '.agents.defaults.contextTokens // 200000' "$backup_path" 2>/dev/null || echo "200000")
    new_context=$(jq -r '.agents.defaults.contextTokens // 200000' "$CONFIG_FILE")

    local old_image new_image
    old_image=$(jq -r '.agents.defaults.imageMaxDimensionPx // 1200' "$backup_path" 2>/dev/null || echo "1200")
    new_image=$(jq -r '.agents.defaults.imageMaxDimensionPx // 1200' "$CONFIG_FILE")

    printf "  %-30s %-20s → %s\n" "Heartbeat isolation:" "$old_isolated" "$new_isolated"
    printf "  %-30s %-20s → %s\n" "Heartbeat interval:" "$old_heartbeat_interval" "$new_heartbeat_interval"
    printf "  %-30s %-20s → %s\n" "Heartbeat model:" "(primary model)" "gemini-2.5-flash-lite"
    printf "  %-30s %-20s → %s\n" "Context token cap:" "$old_context" "$new_context"
    printf "  %-30s %-20s → %s\n" "Image max dimension:" "${old_image}px" "${new_image}px"
    printf "  %-30s %-20s → %s\n" "Compaction model:" "(primary model)" "gemini-2.5-flash-lite"

    echo -e "${CYAN}────────────────────────────────────────${NC}"
}

# Estimate savings based on patches applied
estimate_savings() {
    echo ""
    log "Estimated cost reduction:"
    echo -e "${CYAN}────────────────────────────────────────${NC}"
    echo "  Heartbeat isolation:    ~97% less per heartbeat (100K → 3K tokens)"
    echo "  Heartbeat interval:     ~45% fewer heartbeat calls"
    echo "  Heartbeat model:        ~98% cheaper per heartbeat call"
    echo "  Context cap:            Prevents runaway context accumulation"
    echo "  Image downscaling:      ~40% fewer vision tokens"
    echo ""
    echo -e "  ${GREEN}Combined: 30-50% total cost reduction${NC}"
    echo -e "  ${YELLOW}Upgrade to Pro for ClawTK Engine compression: up to 80% total savings${NC}"
    echo -e "${CYAN}────────────────────────────────────────${NC}"
}

# Restore from backup
restore_config() {
    if [ ! -f "$STATE_FILE" ]; then
        err "No ClawTK state file found. Nothing to restore."
        exit 1
    fi

    local backup_path
    backup_path=$(jq -r '.configBackup // empty' "$STATE_FILE")

    if [ -z "$backup_path" ] || [ ! -f "$backup_path" ]; then
        err "Backup file not found at: $backup_path"
        err "You may need to manually restore from ~/.openclaw/openclaw.json.clawtk-backup.*"
        exit 1
    fi

    cp "$backup_path" "$CONFIG_FILE"
    log "Config restored from: $backup_path"
    log "Restart your OpenClaw gateway for changes to take effect."
}

# Main
main() {
    local action="${1:-patch}"

    case "$action" in
        patch)
            check_prereqs
            local backup_path
            backup_path=$(backup_config)
            patch_config "$backup_path"
            show_diff "$backup_path"
            estimate_savings
            echo "$backup_path"  # Return backup path for setup.sh to capture
            ;;
        restore)
            restore_config
            ;;
        *)
            err "Unknown action: $action"
            echo "Usage: patch-config.sh [patch|restore]"
            exit 1
            ;;
    esac
}

main "$@"


path: scripts/setup.sh
content: #!/usr/bin/env bash
# ClawTK Setup — One-command OpenClaw cost optimization
# Usage:
#   setup.sh              Run full setup
#   setup.sh --status     Show current optimization status
#   setup.sh --restore    Restore original config
#   setup.sh --override   Temporarily disable spend caps (1 hour)

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
OPENCLAW_DIR="${OPENCLAW_DIR:-$HOME/.openclaw}"
STATE_FILE="$OPENCLAW_DIR/clawtk-state.json"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'

log()  { echo -e "${GREEN}[clawtk]${NC} $1"; }
warn() { echo -e "${YELLOW}[clawtk]${NC} $1"; }
err()  { echo -e "${RED}[clawtk]${NC} $1" >&2; }

# ── Full Setup ───────────────────────────────────────────────────────────────

run_setup() {
    echo -e "${BOLD}"
    echo "  ╔═══════════════════════════════════════╗"
    echo "  ║         ClawTK Setup v1.0.0         ║"
    echo "  ║   Cut your OpenClaw costs by 60-80%   ║"
    echo "  ╚═══════════════════════════════════════╝"
    echo -e "${NC}"

    # Step 1: Patch config
    log "Step 1/3: Optimizing OpenClaw config..."
    local backup_path
    backup_path=$(bash "$SCRIPT_DIR/patch-config.sh" patch | tail -1)

    # Step 2: Register hooks
    log "Step 2/3: Installing spend guard hooks..."
    if command -v openclaw &>/dev/null; then
        openclaw plugins install "$SKILL_DIR" 2>/dev/null || {
            warn "Could not register hook pack automatically."
            warn "Run manually: openclaw plugins install $SKILL_DIR"
        }
    else
        warn "openclaw CLI not found in PATH. Hooks will activate on next session."
    fi

    # Step 3: Write state (preserve existing tier/license if upgrading)
    log "Step 3/4: Saving setup state..."
    local tier="free"
    local license_key="null"
    local rtk_installed="false"
    local rtk_version="null"
    if [ -f "$STATE_FILE" ]; then
        tier=$(jq -r '.tier // "free"' "$STATE_FILE")
        license_key=$(jq -r '.licenseKey // "null"' "$STATE_FILE")
        rtk_installed=$(jq -r '.rtkInstalled // "false"' "$STATE_FILE")
        rtk_version=$(jq -r '.rtkVersion // "null"' "$STATE_FILE")
    fi

    cat > "$STATE_FILE" <<EOF
{
  "tier": "$tier",
  "version": "1.0.0",
  "installDate": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
  "configBackup": "$backup_path",
  "licenseKey": $([ "$license_key" = "null" ] && echo "null" || echo "\"$license_key\""),
  "rtkInstalled": $rtk_installed,
  "rtkVersion": $([ "$rtk_version" = "null" ] && echo "null" || echo "\"$rtk_version\""),
  "spendCaps": {
    "daily": 10,
    "weekly": 50
  },
  "overrideUntil": null
}
EOF

    # Step 4: Install Pro features if tier is pro/cloud
    if [ "$tier" = "pro" ] || [ "$tier" = "cloud" ]; then
        log "Step 4/4: Setting up Pro features..."

        # Install ClawTK Engine if not already installed
        if [ "$rtk_installed" != "true" ]; then
            log "Installing ClawTK Engine token compression..."
            bash "$SCRIPT_DIR/install-engine.sh" install || {
                warn "Engine installation failed. You can retry with: /clawtk setup"
            }
        else
            log "Engine already installed. Checking version..."
            bash "$SCRIPT_DIR/install-engine.sh" check >/dev/null 2>&1 || {
                warn "Engine may need updating. Run: brew upgrade rtk"
            }
        fi
    else
        log "Step 4/4: Skipped (Pro features require activation)"
    fi

    echo ""
    echo -e "${GREEN}${BOLD}Setup complete!${NC}"
    echo ""
    echo "What's active now:"
    echo "  [x] Heartbeat isolation (biggest saver)"
    echo "  [x] Optimized heartbeat interval (55m)"
    echo "  [x] Cheap heartbeat model (Flash Lite)"
    echo "  [x] Context cap (100K tokens)"
    echo "  [x] Image downscaling (800px)"
    echo "  [x] Spend caps (\$10/day, \$50/week)"
    echo "  [x] Retry loop protection"

    if [ "$tier" = "pro" ] || [ "$tier" = "cloud" ]; then
        # Re-read state to get updated Engine status
        local rtk_now
        rtk_now=$(jq -r '.rtkInstalled // false' "$STATE_FILE")
        echo "  $([ "$rtk_now" = "true" ] && echo "[x]" || echo "[ ]") ClawTK Engine token compression (Pro)"
        echo "  [x] Semantic task caching (Pro)"
        echo "  [x] Custom spend caps (Pro)"
    fi

    echo ""
    echo "Commands:"
    echo "  /clawtk savings   — See how much you're saving"
    echo "  /clawtk status    — Check optimization status"
    echo "  /clawtk restore   — Undo all changes"
    echo "  /clawtk override  — Temporarily disable caps (1hr)"

    if [ "$tier" = "free" ]; then
        echo ""
        echo -e "${YELLOW}Upgrade to Pro (\$49 one-time) for ClawTK Engine compression — up to 80% savings.${NC}"
        echo -e "${YELLOW}Visit https://clawtk.co/pro${NC}"
    fi

    echo ""
    log "Restart your OpenClaw gateway for all changes to take effect."
}

# ── Status ───────────────────────────────────────────────────────────────────

show_status() {
    if [ ! -f "$STATE_FILE" ]; then
        err "ClawTK is not set up yet. Run /clawtk setup first."
        exit 1
    fi

    local tier install_date rtk_installed override_until
    tier=$(jq -r '.tier' "$STATE_FILE")
    install_date=$(jq -r '.installDate' "$STATE_FILE")
    rtk_installed=$(jq -r '.rtkInstalled' "$STATE_FILE")
    override_until=$(jq -r '.overrideUntil // "none"' "$STATE_FILE")

    echo -e "${BOLD}ClawTK Status${NC}"
    echo -e "${CYAN}────────────────────────────────────────${NC}"
    printf "  %-22s %s\n" "Tier:" "$(echo "$tier" | tr '[:lower:]' '[:upper:]')"
    printf "  %-22s %s\n" "Installed:" "$install_date"
    printf "  %-22s %s\n" "ClawTK Engine Compression:" "$([ "$rtk_installed" = "true" ] && echo "Active" || echo "Not installed (Pro)")"
    printf "  %-22s %s\n" "Spend Cap Override:" "$override_until"
    echo -e "${CYAN}────────────────────────────────────────${NC}"

    echo ""
    echo "Config optimizations:"

    local config_file="$OPENCLAW_DIR/openclaw.json"
    if [ -f "$config_file" ]; then
        local isolated interval context_cap image_dim
        isolated=$(jq -r '.agents.defaults.heartbeat.isolatedSession // false' "$config_file")
        interval=$(jq -r '.agents.defaults.heartbeat.every // "not set"' "$config_file")
        context_cap=$(jq -r '.agents.defaults.contextTokens // "not set"' "$config_file")
        image_dim=$(jq -r '.agents.defaults.imageMaxDimensionPx // "not set"' "$config_file")

        local check_mark="[x]"
        local cross_mark="[ ]"

        echo "  $([ "$isolated" = "true" ] && echo "$check_mark" || echo "$cross_mark") Heartbeat isolation"
        echo "  $([ "$interval" = "55m" ] && echo "$check_mark" || echo "$cross_mark") Heartbeat interval (55m)"
        echo "  $check_mark Heartbeat model (cheap)"
        echo "  $([ "$context_cap" = "100000" ] && echo "$check_mark" || echo "$cross_mark") Context cap (100K)"
        echo "  $([ "$image_dim" = "800" ] && echo "$check_mark" || echo "$cross_mark") Image downscaling (800px)"
    else
        warn "Config file not found at $config_file"
    fi

    # Pro features
    if [ "$tier" = "pro" ] || [ "$tier" = "cloud" ]; then
        echo ""
        echo "Pro features:"
        if [ "$rtk_installed" = "true" ] && command -v rtk &>/dev/null; then
            local rtk_ver
            rtk_ver=$(rtk --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
            echo "  [x] ClawTK Engine compression (v$rtk_ver)"
        else
            echo "  [ ] ClawTK Engine compression (not installed — run /clawtk setup to retry)"
        fi
        echo "  [x] Semantic task caching"
        echo "  [x] Custom spend caps"

        # Cache stats
        local cache_db="$OPENCLAW_DIR/clawtk-cache.db"
        if [ -f "$cache_db" ] && command -v sqlite3 &>/dev/null; then
            local cache_entries cache_hits
            cache_entries=$(sqlite3 "$cache_db" "SELECT count(*) FROM cache" 2>/dev/null || echo "0")
            cache_hits=$(sqlite3 "$cache_db" "SELECT sum(hit_count) FROM cache" 2>/dev/null || echo "0")
            echo ""
            echo "  Cache: $cache_entries entries, $cache_hits total hits"
        fi
    else
        echo ""
        echo -e "  ${YELLOW}Upgrade to Pro for ClawTK Engine compression, caching, and custom caps.${NC}"
        echo -e "  ${YELLOW}https://clawtk.co/pro${NC}"
    fi
}

# ── Override ─────────────────────────────────────────────────────────────────

set_override() {
    if [ ! -f "$STATE_FILE" ]; then
        err "ClawTK is not set up yet. Run /clawtk setup first."
        exit 1
    fi

    # Set override for 1 hour from now
    local override_until
    if [[ "$OSTYPE" == "darwin"* ]]; then
        override_until=$(date -u -v+1H +%Y-%m-%dT%H:%M:%SZ)
    else
        override_until=$(date -u -d "+1 hour" +%Y-%m-%dT%H:%M:%SZ)
    fi

    local updated
    updated=$(jq --arg until "$override_until" '.overrideUntil = $until' "$STATE_FILE")
    echo "$updated" > "$STATE_FILE"

    log "Spend caps disabled until $override_until"
    warn "Caps will re-enable automatically in 1 hour."
}

# ── Restore ──────────────────────────────────────────────────────────────────

run_restore() {
    bash "$SCRIPT_DIR/patch-config.sh" restore
    log "Restart your OpenClaw gateway for changes to take effect."
}

# ── Main ─────────────────────────────────────────────────────────────────────

main() {
    case "${1:-}" in
        --status)  show_status ;;
        --restore) run_restore ;;
        --override) set_override ;;
        *)         run_setup ;;
    esac
}

main "$@"


path: scripts/sync.sh
content: #!/usr/bin/env bash
# ClawTK Cloud Sync — push local spend data to api.clawtk.co
# Usage:
#   sync.sh               Sync new entries since last sync
#   sync.sh --compact     Sync + rewrite JSONL to remove synced entries

set -euo pipefail

OPENCLAW_DIR="${OPENCLAW_DIR:-$HOME/.openclaw}"
STATE_FILE="$OPENCLAW_DIR/clawtk-state.json"
SPEND_LOG="$OPENCLAW_DIR/clawtk-spend.jsonl"
SYNC_OFFSET="$OPENCLAW_DIR/.clawtk-sync-offset"
API_BASE="https://api.clawtk.co/v1"
BATCH_SIZE=500

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

log()  { echo -e "${GREEN}[clawtk]${NC} $1"; }
warn() { echo -e "${YELLOW}[clawtk]${NC} $1"; }
err()  { echo -e "${RED}[clawtk]${NC} $1" >&2; }

# ── Preflight ────────────────────────────────────────────────────────────────

if [ ! -f "$STATE_FILE" ]; then
    err "ClawTK is not set up. Run /clawtk setup first."
    exit 1
fi

tier=$(jq -r '.tier' "$STATE_FILE")
license_key=$(jq -r '.licenseKey // empty' "$STATE_FILE")

if [ "$tier" = "free" ]; then
    err "Cloud sync requires Pro or Cloud tier."
    err "Upgrade at https://clawtk.co/pro"
    exit 1
fi

if [ -z "$license_key" ]; then
    err "No license key found. Run /clawtk activate YOUR-KEY first."
    exit 1
fi

if [ ! -f "$SPEND_LOG" ]; then
    log "No spend data to sync."
    exit 0
fi

# ── Read Offset ──────────────────────────────────────────────────────────────

offset=0
if [ -f "$SYNC_OFFSET" ]; then
    offset=$(cat "$SYNC_OFFSET")
fi

total_lines=$(wc -l < "$SPEND_LOG" | tr -d ' ')

if [ "$offset" -ge "$total_lines" ]; then
    log "Already up to date. No new entries to sync."
    exit 0
fi

new_lines=$((total_lines - offset))
log "Found $new_lines new entries to sync."
log "Data sent: timestamp, token count, estimated cost, tool name, hash."
log "No message content, file contents, or conversation data is included."

# ── Batch and Send ───────────────────────────────────────────────────────────

synced=0
current_offset=$((offset + 1))

while [ "$current_offset" -le "$total_lines" ]; do
    # Extract batch using sed (line range)
    end_line=$((current_offset + BATCH_SIZE - 1))
    if [ "$end_line" -gt "$total_lines" ]; then
        end_line=$total_lines
    fi

    # Build JSON array from JSONL lines
    batch=$(sed -n "${current_offset},${end_line}p" "$SPEND_LOG" | jq -s '.')

    batch_size=$(echo "$batch" | jq 'length')

    if [ "$batch_size" -eq 0 ]; then
        break
    fi

    # POST to API
    response=$(curl -sS --max-time 30 \
        -H "Content-Type: application/json" \
        -H "X-License-Key: $license_key" \
        -d "{\"entries\": $batch}" \
        "$API_BASE/spend" 2>&1)

    # Check response
    ingested=$(echo "$response" | jq -r '.ingested // 0' 2>/dev/null)

    if [ "$ingested" = "0" ] && echo "$response" | jq -e '.error' &>/dev/null; then
        error_msg=$(echo "$response" | jq -r '.error')
        err "API error: $error_msg"
        err "Synced $synced entries before failure."
        # Save partial progress
        echo "$((current_offset - 1))" > "$SYNC_OFFSET"
        exit 1
    fi

    synced=$((synced + batch_size))
    current_offset=$((end_line + 1))

    log "Synced batch: $synced / $new_lines entries"
done

# ── Update Offset ────────────────────────────────────────────────────────────

echo "$total_lines" > "$SYNC_OFFSET"
log "Sync complete. $synced entries pushed to clawtk.co"

# Check for alerts that fired
alerts=$(echo "$response" | jq -r '.alerts // [] | join(", ")' 2>/dev/null)
if [ -n "$alerts" ] && [ "$alerts" != "" ]; then
    warn "Alerts triggered: $alerts"
fi

# ── Optional Compact ─────────────────────────────────────────────────────────

if [ "${1:-}" = "--compact" ] && [ "$total_lines" -gt 100000 ]; then
    log "Compacting spend log (removing $total_lines synced entries)..."

    # Keep only unsynced lines (there shouldn't be any after a full sync,
    # but new entries may have been appended during sync)
    current_total=$(wc -l < "$SPEND_LOG" | tr -d ' ')
    if [ "$current_total" -gt "$total_lines" ]; then
        tail -n $((current_total - total_lines)) "$SPEND_LOG" > "$SPEND_LOG.tmp"
        mv "$SPEND_LOG.tmp" "$SPEND_LOG"
        echo "0" > "$SYNC_OFFSET"
        log "Compacted: kept $((current_total - total_lines)) unsynced entries."
    else
        : > "$SPEND_LOG"
        echo "0" > "$SYNC_OFFSET"
        log "Compacted: spend log cleared."
    fi
fi


path: scripts/uninstall.sh
content: #!/usr/bin/env bash
# ClawTK Uninstall — Clean removal with config restore

set -euo pipefail

OPENCLAW_DIR="${OPENCLAW_DIR:-$HOME/.openclaw}"
STATE_FILE="$OPENCLAW_DIR/clawtk-state.json"
SPEND_LOG="$OPENCLAW_DIR/clawtk-spend.jsonl"
CACHE_DB="$OPENCLAW_DIR/clawtk-cache.db"
CONFIG_FILE="$OPENCLAW_DIR/openclaw.json"
SKILL_DIR="$OPENCLAW_DIR/skills/clawtk"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

log()  { echo -e "${GREEN}[clawtk]${NC} $1"; }
warn() { echo -e "${YELLOW}[clawtk]${NC} $1"; }
err()  { echo -e "${RED}[clawtk]${NC} $1" >&2; }

echo ""
log "Uninstalling ClawTK..."
echo ""

# Step 1: Restore config from backup
if [ -f "$STATE_FILE" ]; then
    backup_path=$(jq -r '.configBackup // empty' "$STATE_FILE")

    if [ -n "$backup_path" ] && [ -f "$backup_path" ]; then
        cp "$backup_path" "$CONFIG_FILE"
        log "Config restored from: $backup_path"
    else
        warn "No config backup found. Your current config is unchanged."
        warn "You may want to manually review: $CONFIG_FILE"
    fi
fi

# Step 2: Unregister hooks
if command -v openclaw &>/dev/null; then
    openclaw plugins uninstall clawtk 2>/dev/null || true
    log "Hooks unregistered."
fi

# Step 3: Remove ClawTK files
rm -f "$STATE_FILE"
log "Removed state file."

rm -f "$SPEND_LOG"
log "Removed spend log."

rm -f "$CACHE_DB"
log "Removed cache database."

# Remove backup files
rm -f "$CONFIG_FILE".clawtk-backup.* 2>/dev/null || true
log "Removed config backups."

# Step 4: Remove skill directory
if [ -d "$SKILL_DIR" ]; then
    rm -rf "$SKILL_DIR"
    log "Removed skill directory."
fi

echo ""
log "ClawTK has been completely removed."
log "Restart your OpenClaw gateway for changes to take effect."
echo ""


path: templates/rtk-version.txt
content: 0.28.0


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)