Skill: cwendler/kemia (version 1.1.0)

--- name: kemia description: > Connect your OpenClaw instance to kemia (https://kemia.byte5.ai) for visual agent configuration management. Use when: (1) connecting to a kemia deployment for the first time (/connect), (2) importing an edited agent config from kemia (/import), (3) checking connection and deploy status (/kemia-status), (4) generating a one-time login link for the kemia web UI (/kemia-link). NOT for: editing agent configs directly — that happens in the kemia web UI. --- # Kemia — Agent Configuration Manager Manage your agent configurations through the kemia web interface. **Architecture:** OpenClaw always initiates. Kemia never pushes. All communication is pull-based via the v1 API. ## Prerequisites - `jq` must be installed - `curl` must be available - Config stored at: `~/.openclaw/workspace/skills/kemia/config.json` ## Commands ### `/connect <kemia-url>` (two-step, resumable) Connect this OpenClaw instance to a kemia deployment. Works in two steps because the confirmation happens in a browser and can take arbitrary time — the script never runs a long polling loop that could be killed by OpenClaw/Exec. **Step 1 — start enrollment:** `/kemia connect https://kemia.byte5.ai` - `POST /api/v1/enroll` → enrollment URL - Persists state to `skills/kemia/enrollment.json` - Prints the URL. **Exits immediately.** **Step 2 — user clicks, confirms in the browser, returns to the agent.** **Step 3 — complete:** `/kemia connect` (no argument) - Reads `enrollment.json`, polls `/status` **once**. - If still pending → tells the user to wait and run `/kemia connect` again; exits. - If completed → saves `config.json`, exports workspace `.md` files, cleans up state. - If expired → cleans up, asks the user to re-run with a URL. **Already connected?** A fresh `/kemia connect` without arguments is safe: the script verifies `config.json` against `/api/v1/status` first. If the key works, it reports "already connected" and exits without touching anything. **Re-connecting from the same machine:** the script sends a stable machine fingerprint (`sha256(hostname + workspace_path)`) with the enrollment request. If kemia recognizes the machine, the confirmation page offers **Re-Authenticate** (rotate the API key on the existing instance, keep all agents) or **Set up as a new instance** (fresh start). Duplicates are no longer silently created. **No shared secrets needed.** The enrollment URL is the authentication. **Script:** `scripts/connect.sh [kemia-url]` **Environment variables (optional):** - `OPENCLAW_WORKSPACE` — workspace path (default: `~/.openclaw/workspace`) - `OPENCLAW_INSTANCE_NAME` — instance name (default: hostname) - `OPENCLAW_AGENT_NAME` — agent name (default: `CyberClaw`) --- ### `/kemia-link` Generate a one-time login URL for the kemia web interface (15-minute expiry). **Script:** `scripts/kemia-link.sh` Use this when the user needs a fresh session — e.g. after logout or on a new device. --- ### `/import [agent-id]` Import the latest deploy-ready snapshot from kemia into the workspace. **Script:** `scripts/import.sh [agent-id]` - Fetches the snapshot marked "deploy ready" in the kemia UI - Backs up current workspace `.md` files to `.kemia-backup/` before overwriting - If no `agent-id` given, uses the saved ID from `config.json` After import, restart OpenClaw for changes to take effect. --- ### `/kemia-status` Check connection health and whether a deploy-ready snapshot is pending. **Script:** `scripts/status.sh` --- ## Config File After `/connect`, credentials are stored in: ``` ~/.openclaw/workspace/skills/kemia/config.json ``` ```json { "apiKey": "km_...", "instanceId": "clxyz...", "baseUrl": "https://kemia.byte5.ai", "agentId": "clxyz..." } ``` --- ## Setup Flow 1. Run `/kemia connect https://kemia.byte5.ai` (or your kemia instance URL) — prints enrollment URL, exits. 2. Click the enrollment URL — confirm in the browser → logged into kemia. 3. Run `/kemia connect` again (no arguments) — completes setup, exports agent config. 4. Shape the config in the kemia web UI (Persona, Role, Knowledge, Team). 5. Create a snapshot → mark it "deploy ready". 6. Run `/import` → restart OpenClaw. --- ## API Reference For full endpoint documentation see `references/api.md`.

path: references/api.md
content: # kemia API v1 — Reference

Base URL: `https://kemia.byte5.ai` (or your self-hosted instance)

## Authentication

### Enrollment (initial setup)

The enrollment flow creates a new kemia instance and returns an API key. No shared secrets required.

```
POST /api/v1/enroll              → { enrollUrl, pollUrl }  (no auth)
GET  /api/v1/enroll/:code/status → { status, apiKey? }     (no auth, code is auth)
POST /api/v1/enroll/:code/ack    → { status: "acked" }     (no auth, code is auth)
```

### Bearer Token (all other endpoints)

After enrollment, all API calls use the API key as a Bearer token:

```
[REDACTED_SECRET]
```

The API key is returned once during enrollment polling and never stored in plaintext on the server.

---

## Enrollment Flow

### Step 1: Start enrollment

```
POST /api/v1/enroll
```

**Auth:** None (rate-limited: 5/min per IP)

**Request:**
```json
{
  "name": "My Instance",
  "orchestrator": "openclaw",
  "fingerprint": "sha256:abc123..."
}
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `name` | string | "My Instance" | Display name for the kemia instance |
| `orchestrator` | string | "openclaw" | Orchestrator type: `"openclaw"`, `"paperclip"`, or custom |
| `fingerprint` | string | *(none)* | Optional stable ID for this machine (e.g. `sha256(hostname + workspace_path)`). If it matches an existing instance, the confirm page offers Re-Auth instead of silently creating a duplicate. Max 200 chars. |

**Response (200):**
```json
{
  "enrollUrl": "https://kemia.byte5.ai/enroll/km_enr_abc123...",
  "pollUrl": "/api/v1/enroll/km_enr_abc123.../status",
  "expiresAt": "2026-03-28T12:15:00.000Z"
}
```

The orchestrator shows `enrollUrl` to the human operator. The code is in the URL — no manual entry needed. Expires in 15 minutes, single-use.

### Step 2: Human confirms

The human clicks `enrollUrl` → lands on a confirmation page → clicks "Connect" → instance + session created → redirected to dashboard.

### Step 3: Poll for API key

```
GET /api/v1/enroll/:code/status
```

**Auth:** None (the code IS the auth; rate-limited: 30/min per IP)

**Response (pending):**
```json
{ "status": "pending" }
```

**Response (completed — API key returned within the retention window):**
```json
{
  "status": "completed",
  "instanceId": "clxyz...",
  "apiKey": "km_abc123..."
}
```

The API key is retrievable for a short retention window (5 minutes) after
confirmation. Within that window, repeated polls return the same key — this
lets a client that crashes mid-save recover without re-enrolling. After the
window elapses, the next poll clears the stored key and returns without it:

```json
{ "status": "completed", "instanceId": "clxyz..." }
```

Clients that have persisted the key should call `POST .../ack` to clear it
server-side immediately, rather than waiting out the window.

**Response (expired):**
```json
{ "status": "expired" }
```

### Step 4 (optional): Ack the stored key

```
POST /api/v1/enroll/:code/ack
```

**Auth:** None (the code IS the auth; rate-limited: 30/min per IP)

Signals that the client has persisted the API key and the server may clear
its stored (encrypted) copy now. Idempotent.

**Response (200):**
```json
{ "status": "acked" }
```

**Response (409):** Enrollment not yet completed.

---

## Endpoints

### `GET /api/v1/status`

Health check. Verifies API key is valid and returns instance identity.

**Auth:** Bearer token

**Response (200):**
```json
{
  "ok": true,
  "instanceId": "clxyz...",
  "instanceName": "my-instance"
}
```

---

### `POST /api/v1/agents`

Export/push agent configuration to kemia. Upserts the agent and fully replaces its config files.

**Auth:** Bearer token

**Request:**
```json
{
  "name": "CyberClaw",
  "description": "Main agent",
  "files": [
    { "filename": "SOUL.md", "content": "# Soul\n..." },
    { "filename": "IDENTITY.md", "content": "..." }
  ]
}
```

**Validation:**
- `name`: 1–100 chars, alphanumeric + hyphens/underscores (`^[a-zA-Z0-9_-]+$`)
- `files[].filename`: must match `^[A-Z][A-Z0-9_]*\.md$` (e.g. `SOUL.md`)
- `files[].content`: max 500KB per file
- `files`: 1–20 files per agent

**Response (200):**
```json
{
  "agentId": "clxyz...",
  "name": "CyberClaw"
}
```

---

### `GET /api/v1/agents/:agentId`

Get agent details and current config files.

**Auth:** Bearer token

**Response (200):**
```json
{
  "agent": {
    "id": "clxyz...",
    "name": "CyberClaw",
    "description": "Main agent",
    "createdAt": "2026-03-10T...",
    "updatedAt": "2026-03-10T..."
  },
  "files": [
    {
      "id": "clfile...",
      "filename": "SOUL.md",
      "content": "...",
      "updatedAt": "2026-03-10T..."
    }
  ]
}
```

---

### `GET /api/v1/agents/:agentId/profile`

Compact agent profile for external consumers (Paperclip plugin, dashboards, etc.).

**Auth:** Bearer token

**Response (200):**
```json
{
  "id": "clxyz...",
  "name": "CyberClaw",
  "description": "Main agent",
  "healthScore": 95,
  "healthLevel": "healthy",
  "persona": {
    "formality": 30,
    "directness": 70,
    "warmth": 80,
    "humor": 60,
    "sarcasm": 10,
    "conciseness": 50,
    "proactivity": 70,
    "autonomy": 60,
    "riskTolerance": 40,
    "creativity": 65,
    "drama": 30,
    "philosophy": 45
  },
  "role": {
    "jobTitle": "Customer Support",
    "taskCount": 4,
    "boundaryCount": 3,
    "ruleCount": 5
  },
  "knowledge": {
    "sourceCount": 2,
    "capabilityCount": 5
  },
  "team": {
    "humanInterface": true,
    "escalationTarget": "Team Lead"
  },
  "fileCount": 3,
  "starterCount": 4,
  "updatedAt": "2026-03-28T..."
}
```

---

### `GET /api/v1/agents/:agentId/deploy`

Fetch the deploy-ready snapshot for an agent. This is what orchestrators poll to import edited configs.

**Auth:** Bearer token

**Response (200):**
```json
{
  "snapshot": {
    "id": "clsnap...",
    "name": "v2-tweaks",
    "description": "Adjusted personality",
    "createdAt": "2026-03-10T..."
  },
  "files": [
    { "filename": "SOUL.md", "content": "..." },
    { "filename": "IDENTITY.md", "content": "..." }
  ]
}
```

**Response (404):** No deploy-ready snapshot exists.

---

### `GET /api/v1/agents/:agentId/health`

Agent Guardian health check — config drift detection.

**Auth:** Bearer token

**Response (200):**
```json
{
  "health": {
    "score": 85,
    "level": "warning",
    "files": [
      { "filename": "SOUL.md", "status": "aligned" },
      { "filename": "AGENTS.md", "status": "drifted" }
    ],
    "suggestions": ["AGENTS.md has changed since last deploy"],
    "hasDeployBaseline": true
  }
}
```

---

### `POST /api/v1/auth-token`

Generate a one-time login URL for the web interface. Use this when the user needs a new session (e.g. expired, different device).

**Auth:** Bearer token

**Response (200):**
```json
{
  "token": "raw-token-value",
  "url": "https://kemia.byte5.ai/auth/token?t=raw-token-value",
  "expiresAt": "2026-03-10T12:15:00.000Z",
  "message": "Send this URL to the user. It expires in 15 minutes and can only be used once."
}
```

---

### `POST /api/v1/connect` (DEPRECATED)

**Status: 410 Gone.** Use `POST /api/v1/enroll` instead.

---

## Error Responses

All errors follow this format:
```json
{ "error": "Human-readable error message" }
```

| Status | Meaning |
|--------|---------|
| 400 | Validation error (bad input) |
| 401 | Missing/invalid API key |
| 404 | Resource not found |
| 409 | Conflict (e.g. enrollment already used) |
| 410 | Gone (deprecated endpoint) |
| 429 | Rate limited — check `Retry-After` header |
| 500 | Server error |

---

## Integration Guide

### For OpenClaw Skills

```bash
# 1. Start enrollment
RESPONSE=$(curl -sf -X POST -H "Content-Type: application/json" \
  -d '{"name":"My Agent","orchestrator":"openclaw"}' \
  https://kemia.byte5.ai/api/v1/enroll)

# 2. Show URL to user
echo "$RESPONSE" | jq -r '.enrollUrl'

# 3. Poll until confirmed
while true; do
  STATUS=$(curl -sf "https://kemia.byte5.ai$(echo $RESPONSE | jq -r '.pollUrl')")
  [ "$(echo $STATUS | jq -r '.status')" = "completed" ] && break
  sleep 10
done

# 4. Save API key
echo "$STATUS" | jq -r '.apiKey'
```

### For Paperclip Plugins

```typescript
// 1. Start enrollment
const { enrollUrl, pollUrl } = await ctx.http
  .fetch(`${kemiaUrl}/api/v1/enroll`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ name: "Paperclip", orchestrator: "paperclip" }),
  })
  .then((r) => r.json());

// 2. Show enrollUrl to user (in Board UI)

// 3. Poll for completion
const { status, apiKey } = await ctx.http
  .fetch(`${kemiaUrl}${pollUrl}`)
  .then((r) => r.json());

// 4. Store apiKey in plugin state
```

---

## Data Model

```
Instance (tenant)
  ├── Agent[]
  │     ├── ConfigFile[]         (current working state)
  │     ├── Snapshot[]           (versioned checkpoints)
  │     │     └── SnapshotFile[]
  │     ├── KnowledgeSource[]    (domain knowledge)
  │     ├── Capability[]         (tool toggles)
  │     └── ConversationStarter[]
  ├── Team[]
  │     └── TeamEdge[]           (agent relationships)
  ├── Deployment[]               (cross-instance pushes)
  ├── AuthToken[]                (one-time login links)
  └── Enrollment[]               (pending/completed enrollments)
```

- One Instance = one orchestrator deployment (OpenClaw, Paperclip, etc.)
- An Instance can have multiple Agents
- Each Agent has ConfigFiles (live state) and Snapshots (versions)
- A Snapshot can be marked `deployReady` for import by the orchestrator


path: scripts/connect.sh
content: #!/usr/bin/env bash
set -euo pipefail

# =============================================================================
# kemia connect — Register this OpenClaw instance via enrollment flow
#
# Usage: connect.sh <kemia-url>
#
# Resumable two-step design:
#   First call (no state):    POST /enroll → persist enrollment.json → show URL → EXIT
#   User clicks URL, confirms in browser, returns to OpenClaw.
#   Second call (state present): poll once →
#       pending   → "still waiting, try again in a moment"
#       completed → save config.json, export agents, clean up
#       expired   → clean up, tell user to re-run
#
# No long-running polling process — survives OpenClaw/Exec killing the shell.
# =============================================================================

KEMIA_URL_ARG="${1:-}"

WORKSPACE="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}"
SKILL_DIR="${WORKSPACE}/skills/kemia"
CONFIG_FILE="${SKILL_DIR}/config.json"
STATE_FILE="${SKILL_DIR}/enrollment.json"
INSTANCE_NAME="${OPENCLAW_INSTANCE_NAME:-$(hostname)}"
AGENT_NAME="${OPENCLAW_AGENT_NAME:-CyberClaw}"

command -v jq >/dev/null 2>&1 || { echo "ERROR: jq is required. Install with: apt-get install jq"; exit 1; }
command -v curl >/dev/null 2>&1 || { echo "ERROR: curl is required."; exit 1; }

mkdir -p "${SKILL_DIR}"

# ─── Helpers ───────────────────────────────────────────────────────────────

json_get() {
  # $1 = file, $2 = jq expression
  jq -r "$2 // empty" "$1" 2>/dev/null || true
}

now_epoch() { date -u +%s; }

sha256_stdin() {
  # Portable sha256 of stdin → hex digest (no filename suffix)
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum | awk '{print $1}'
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 | awk '{print $1}'
  else
    echo ""
  fi
}

compute_fingerprint() {
  # Stable per (hostname, workspace) pair. Used by kemia to offer Re-Auth
  # instead of silently creating a duplicate instance when /connect is re-run
  # on the same machine. Hash means no hostname leakage.
  local raw digest
  raw="$(hostname 2>/dev/null || echo unknown)::${WORKSPACE}"
  digest=$(printf "%s" "${raw}" | sha256_stdin)
  if [ -n "${digest}" ]; then
    printf "sha256:%s" "${digest}"
  fi
}

iso_to_epoch() {
  # Portable ISO-8601 → epoch (GNU date, BSD date fallback)
  if date -u -d "$1" +%s >/dev/null 2>&1; then
    date -u -d "$1" +%s
  else
    date -u -j -f "%Y-%m-%dT%H:%M:%S" "${1%.*}" +%s 2>/dev/null || echo "0"
  fi
}

export_agent_files() {
  # $1 = kemia base URL, $2 = api key → writes agentId back to config.json
  local base="$1" key="$2"
  local files_json="[]"

  for md_file in SOUL.md IDENTITY.md USER.md MEMORY.md AGENTS.md TOOLS.md HEARTBEAT.md; do
    local filepath="${WORKSPACE}/${md_file}"
    if [ -f "${filepath}" ]; then
      local content
      content=$(jq -Rs '.' < "${filepath}")
      files_json=$(echo "${files_json}" | jq --arg fn "${md_file}" --argjson ct "${content}" '. + [{"filename": $fn, "content": $ct}]')
      echo "  ✓ ${md_file}"
    fi
  done

  if [ "$(echo "${files_json}" | jq 'length')" = "0" ]; then
    echo "  (no workspace .md files found — skipping export)"
    return 0
  fi

  local payload
  payload=$(jq -n --arg name "${AGENT_NAME}" --argjson files "${files_json}" '{name: $name, files: $files}')

  local response
  response=$(curl -sf -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${key}" \
    -d "${payload}" \
    "${base}/api/v1/agents") || {
    echo "ERROR: Agent export failed (HTTP error). Connection is saved; you can re-run /connect to retry export."
    return 1
  }

  local agent_id
  agent_id=$(echo "${response}" | jq -r '.agentId // empty')
  if [ -n "${agent_id}" ]; then
    jq --arg aid "${agent_id}" '. + {agentId: $aid}' "${CONFIG_FILE}" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "${CONFIG_FILE}"
    chmod 600 "${CONFIG_FILE}"
    echo "✓ Agent '${AGENT_NAME}' exported (id: ${agent_id})"
  fi
}

finish_enrollment() {
  # $1 = status response JSON (must be status=completed)
  local response="$1"
  local api_key instance_id kemia_url code
  [REDACTED_SECRET] "${response}" | jq -r '.apiKey // empty')
  instance_id=$(echo "${response}" | jq -r '.instanceId // empty')
  kemia_url=$(json_get "${STATE_FILE}" '.kemiaUrl')
  code=$(json_get "${STATE_FILE}" '.code')

  if [ -z "${api_key}" ] || [ -z "${instance_id}" ]; then
    echo "ERROR: Enrollment completed but credentials missing in response."
    echo "This can happen if the 5-minute key-retention window elapsed."
    echo "Remove ${STATE_FILE} and run /connect again."
    exit 1
  fi

  cat > "${CONFIG_FILE}" <<EOF
{
  "apiKey": "${api_key}",
  "instanceId": "${instance_id}",
  "baseUrl": "${kemia_url}"
}
EOF
  chmod 600 "${CONFIG_FILE}"

  # Ack the server so it clears the stored encrypted key immediately.
  # Best-effort: failure here is not fatal — the key will be cleared when
  # the 5-minute retention window elapses anyway.
  if [ -n "${code}" ]; then
    curl -sf -X POST "${kemia_url}/api/v1/enroll/${code}/ack" >/dev/null 2>&1 || true
  fi

  rm -f "${STATE_FILE}"

  echo "✓ Connected. Instance: ${instance_id}"
  echo ""
  echo "Exporting agent config files..."
  export_agent_files "${kemia_url}" "${api_key}" || true

  echo ""
  echo "Done. You're logged in at kemia via the enrollment link."
  echo "Run /kemia-link anytime you need a fresh login URL."
}

poll_once() {
  # Reads STATE_FILE, polls once, acts on result. Returns 0 on success/still-pending, 1 on fatal error.
  local kemia_url poll_url
  kemia_url=$(json_get "${STATE_FILE}" '.kemiaUrl')
  poll_url=$(json_get "${STATE_FILE}" '.pollUrl')

  if [ -z "${kemia_url}" ] || [ -z "${poll_url}" ]; then
    echo "ERROR: enrollment.json is malformed. Removing and starting over."
    rm -f "${STATE_FILE}"
    return 1
  fi

  local response
  response=$(curl -sf "${kemia_url}${poll_url}" 2>/dev/null) || {
    echo "ERROR: Could not reach ${kemia_url}. Check connectivity and try again."
    return 1
  }

  local status
  status=$(echo "${response}" | jq -r '.status // empty')

  case "${status}" in
    completed)
      finish_enrollment "${response}"
      ;;
    pending)
      local enroll_url expires_at
      enroll_url=$(json_get "${STATE_FILE}" '.enrollUrl')
      expires_at=$(json_get "${STATE_FILE}" '.expiresAt')
      echo "Enrollment still pending — the user has not yet confirmed in the browser."
      echo ""
      echo "  Link: ${enroll_url}"
      echo "  Expires: ${expires_at}"
      echo ""
      echo "After the user clicks and confirms, run /kemia connect again to complete."
      ;;
    expired)
      echo "Enrollment expired. Cleaning up."
      rm -f "${STATE_FILE}"
      echo "Run /kemia connect <kemia-url> to start a new enrollment."
      return 1
      ;;
    *)
      echo "ERROR: Unexpected enrollment status: ${status}"
      echo "Raw response: ${response}"
      return 1
      ;;
  esac
}

start_new_enrollment() {
  # $1 = kemia base URL
  local kemia_url="$1"
  echo "Starting enrollment with ${kemia_url}..."

  local fingerprint payload response
  fingerprint=$(compute_fingerprint)
  if [ -n "${fingerprint}" ]; then
    payload=$(jq -n --arg name "${INSTANCE_NAME}" --arg fp "${fingerprint}" \
      '{name: $name, orchestrator: "openclaw", fingerprint: $fp}')
  else
    payload=$(jq -n --arg name "${INSTANCE_NAME}" \
      '{name: $name, orchestrator: "openclaw"}')
  fi
  response=$(curl -sf -X POST \
    -H "Content-Type: application/json" \
    -d "${payload}" \
    "${kemia_url}/api/v1/enroll") || {
    echo "ERROR: Could not start enrollment. Check the URL and try again."
    exit 1
  }

  local enroll_url poll_url expires_at
  enroll_url=$(echo "${response}" | jq -r '.enrollUrl')
  poll_url=$(echo "${response}" | jq -r '.pollUrl')
  expires_at=$(echo "${response}" | jq -r '.expiresAt')

  if [ -z "${enroll_url}" ] || [ "${enroll_url}" = "null" ]; then
    echo "ERROR: Invalid response from kemia: ${response}"
    exit 1
  fi

  # Extract code from enrollUrl (last path segment)
  local code="${enroll_url##*/}"

  cat > "${STATE_FILE}" <<EOF
{
  "code": "${code}",
  "enrollUrl": "${enroll_url}",
  "pollUrl": "${poll_url}",
  "expiresAt": "${expires_at}",
  "kemiaUrl": "${kemia_url}"
}
EOF
  chmod 600 "${STATE_FILE}"

  echo ""
  echo "════════════════════════════════════════════"
  echo "  Open this link to connect:"
  echo ""
  echo "  ${enroll_url}"
  echo ""
  echo "  Expires: ${expires_at}"
  echo "════════════════════════════════════════════"
  echo ""
  echo "After the user confirms in the browser, run /kemia connect again"
  echo "(without arguments) to complete the setup."
}

# ─── Main dispatch ─────────────────────────────────────────────────────────

# Case A: already connected — verify and exit
if [ -f "${CONFIG_FILE}" ]; then
  BASE_URL=$(json_get "${CONFIG_FILE}" '.baseUrl')
  [REDACTED_SECRET] "${CONFIG_FILE}" '.apiKey')

  if [ -n "${BASE_URL}" ] && [ -n "${API_KEY}" ]; then
    HTTP_CODE=$(curl -s -o /tmp/kemia-connect-probe.$$ -w "%{http_code}" \
      -H "Authorization: Bearer ${API_KEY}" \
      "${BASE_URL}/api/v1/status" 2>/dev/null || echo "000")

    case "${HTTP_CODE}" in
      200)
        INSTANCE_NAME_LIVE=$(jq -r '.instanceName // "unknown"' /tmp/kemia-connect-probe.$$ 2>/dev/null || echo "unknown")
        rm -f /tmp/kemia-connect-probe.$$
        echo "✓ Already connected to kemia at ${BASE_URL}"
        echo "  Instance: ${INSTANCE_NAME_LIVE}"
        echo ""
        echo "To start fresh, delete ${CONFIG_FILE} and run /kemia connect <url> again."
        exit 0
        ;;
      401|403)
        rm -f /tmp/kemia-connect-probe.$$
        echo "Config exists but API key is not accepted (HTTP ${HTTP_CODE}). Removing stale config."
        rm -f "${CONFIG_FILE}"
        ;;
      000|5*)
        rm -f /tmp/kemia-connect-probe.$$
        echo "ERROR: Could not reach ${BASE_URL} (HTTP ${HTTP_CODE}). Check connectivity."
        echo "Config is preserved. Try /kemia-status once kemia is reachable."
        exit 1
        ;;
      *)
        rm -f /tmp/kemia-connect-probe.$$
        echo "ERROR: Unexpected status check result (HTTP ${HTTP_CODE}). Config preserved."
        exit 1
        ;;
    esac
  fi
fi

# Case B: pending enrollment — resume by polling once
if [ -f "${STATE_FILE}" ]; then
  EXPIRES_AT=$(json_get "${STATE_FILE}" '.expiresAt')
  if [ -n "${EXPIRES_AT}" ]; then
    EXPIRES_EPOCH=$(iso_to_epoch "${EXPIRES_AT}")
    if [ "${EXPIRES_EPOCH}" != "0" ] && [ "$(now_epoch)" -gt "${EXPIRES_EPOCH}" ]; then
      echo "Previous enrollment expired (deadline: ${EXPIRES_AT}). Cleaning up."
      rm -f "${STATE_FILE}"
      # Fall through to Case C
    else
      poll_once
      exit $?
    fi
  fi
fi

# Case C: fresh enrollment — argument required
if [ -z "${KEMIA_URL_ARG}" ]; then
  echo "Usage: /kemia connect <kemia-url>"
  echo ""
  echo "Example: /kemia connect https://kemia.byte5.ai"
  exit 1
fi

start_new_enrollment "${KEMIA_URL_ARG%/}"


path: scripts/import.sh
content: #!/usr/bin/env bash
set -euo pipefail

# =============================================================================
# kemia import — Import deploy-ready snapshot into OpenClaw workspace
#
# Usage: import.sh [agent-id]
#
# Reads config.json for API credentials, fetches the deploy-ready snapshot,
# and writes files back to the OpenClaw workspace root.
# =============================================================================

WORKSPACE="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}"
SKILL_DIR="${WORKSPACE}/skills/kemia"
CONFIG_FILE="${SKILL_DIR}/config.json"

# Require jq
command -v jq &>/dev/null || { echo "ERROR: jq is required."; exit 1; }

# Load config
if [ ! -f "${CONFIG_FILE}" ]; then
  echo "ERROR: Not connected to kemia. Run /connect first."
  exit 1
fi

BASE_URL=$(jq -r '.baseUrl' "${CONFIG_FILE}")
API_KEY=$(jq -r '.apiKey' "${CONFIG_FILE}")
AGENT_ID="${1:-$(jq -r '.agentId // empty' "${CONFIG_FILE}")}"

if [ -z "${AGENT_ID}" ]; then
  echo "ERROR: No agent ID specified and none saved in config."
  echo "Usage: import.sh <agent-id>"
  exit 1
fi

# ---- Fetch deploy-ready snapshot ----
echo "Checking for deploy-ready snapshot (agent: ${AGENT_ID})..."

HTTP_CODE=$(curl -s -o /tmp/kemia-deploy-response.json -w "%{http_code}" \
  -H "Authorization: Bearer ${API_KEY}" \
  "${BASE_URL}/api/v1/agents/${AGENT_ID}/deploy")

if [ "${HTTP_CODE}" = "404" ]; then
  echo "No deploy-ready snapshot found."
  echo "Mark a snapshot as 'deploy ready' in the kemia web interface first."
  exit 0
fi

if [ "${HTTP_CODE}" != "200" ]; then
  echo "ERROR: API returned HTTP ${HTTP_CODE}"
  cat /tmp/kemia-deploy-response.json 2>/dev/null
  exit 1
fi

RESPONSE=$(cat /tmp/kemia-deploy-response.json)
rm -f /tmp/kemia-deploy-response.json

SNAPSHOT_NAME=$(echo "${RESPONSE}" | jq -r '.snapshot.name')
SNAPSHOT_ID=$(echo "${RESPONSE}" | jq -r '.snapshot.id')
FILE_COUNT=$(echo "${RESPONSE}" | jq '.files | length')

echo "✓ Found snapshot '${SNAPSHOT_NAME}' with ${FILE_COUNT} files."

# ---- Backup current files ----
BACKUP_DIR="${WORKSPACE}/.kemia-backup/$(date +%Y%m%d-%H%M%S)"
mkdir -p "${BACKUP_DIR}"

echo "Backing up current files to ${BACKUP_DIR}..."
echo "${RESPONSE}" | jq -r '.files[].filename' | while read -r filename; do
  if [ -f "${WORKSPACE}/${filename}" ]; then
    cp "${WORKSPACE}/${filename}" "${BACKUP_DIR}/${filename}"
    echo "  ↪ ${filename}"
  fi
done

# ---- Write imported files ----
echo "Importing files to workspace..."

echo "${RESPONSE}" | jq -c '.files[]' | while read -r file; do
  FILENAME=$(echo "${file}" | jq -r '.filename')
  echo "${file}" | jq -r '.content' > "${WORKSPACE}/${FILENAME}"
  echo "  ✓ ${FILENAME}"
done

echo ""
echo "Import complete!"
echo "  Snapshot: ${SNAPSHOT_NAME} (${SNAPSHOT_ID})"
echo "  Files:    ${FILE_COUNT}"
echo "  Backup:   ${BACKUP_DIR}"
echo ""
echo "Restart your OpenClaw agent to apply the new config."


path: scripts/kemia-link.sh
content: #!/usr/bin/env bash
set -euo pipefail

# =============================================================================
# kemia-link — Generate a one-time login URL for the kemia web interface
#
# Usage: kemia-link.sh
# =============================================================================

WORKSPACE="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}"
CONFIG_FILE="${WORKSPACE}/skills/kemia/config.json"

command -v jq &>/dev/null || { echo "ERROR: jq is required."; exit 1; }

if [ ! -f "${CONFIG_FILE}" ]; then
  echo "ERROR: Not connected to kemia. Run /connect first."
  exit 1
fi

BASE_URL=$(jq -r '.baseUrl' "${CONFIG_FILE}")
API_KEY=$(jq -r '.apiKey' "${CONFIG_FILE}")

TOKEN_RESPONSE=$(curl -sf \
  -X POST \
  -H "Authorization: Bearer ${API_KEY}" \
  "${BASE_URL}/api/v1/auth-token") || {
  echo "ERROR: Failed to generate login URL. Check your connection with /kemia-status."
  exit 1
}

LOGIN_URL=$(echo "${TOKEN_RESPONSE}" | jq -r '.url')
EXPIRES=$(echo "${TOKEN_RESPONSE}" | jq -r '.expiresAt')

echo "Login URL (single-use, expires in 15 min):"
echo ""
echo "  ${LOGIN_URL}"
echo ""
echo "  Expires: ${EXPIRES}"


path: scripts/status.sh
content: #!/usr/bin/env bash
set -euo pipefail

# =============================================================================
# kemia status — Check connection, pending enrollment, and deploy state
#
# Usage: status.sh
# =============================================================================

WORKSPACE="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}"
SKILL_DIR="${WORKSPACE}/skills/kemia"
CONFIG_FILE="${SKILL_DIR}/config.json"
STATE_FILE="${SKILL_DIR}/enrollment.json"

command -v jq >/dev/null 2>&1 || { echo "ERROR: jq is required."; exit 1; }

# ─── Case A: pending enrollment takes precedence ───────────────────────────
if [ -f "${STATE_FILE}" ]; then
  ENROLL_URL=$(jq -r '.enrollUrl // empty' "${STATE_FILE}")
  EXPIRES=$(jq -r '.expiresAt // empty' "${STATE_FILE}")
  echo "kemia Status"
  echo "════════════"
  echo "  State:       ⏳ PENDING CONFIRMATION"
  echo "  Link:        ${ENROLL_URL}"
  echo "  Expires:     ${EXPIRES}"
  echo ""
  echo "The user must open the link above and confirm in the browser."
  echo "Afterwards, run /kemia connect (no args) to complete."
  exit 0
fi

# ─── Case B: not connected ─────────────────────────────────────────────────
if [ ! -f "${CONFIG_FILE}" ]; then
  echo "Status: NOT CONNECTED"
  echo "Run /kemia connect <kemia-url> to connect."
  exit 0
fi

# ─── Case C: verify connection ─────────────────────────────────────────────
BASE_URL=$(jq -r '.baseUrl' "${CONFIG_FILE}")
API_KEY=$(jq -r '.apiKey' "${CONFIG_FILE}")
INSTANCE_ID=$(jq -r '.instanceId' "${CONFIG_FILE}")
AGENT_ID=$(jq -r '.agentId // empty' "${CONFIG_FILE}")

echo "kemia Status"
echo "════════════"
echo "  Base URL:    ${BASE_URL}"
echo "  Instance ID: ${INSTANCE_ID}"

STATUS_RESPONSE=$(curl -sf -H "Authorization: Bearer ${API_KEY}" "${BASE_URL}/api/v1/status" 2>/dev/null) || {
  echo "  Connection:  ✗ UNREACHABLE"
  echo ""
  echo "Cannot reach kemia or API key is not accepted. Check URL/credentials."
  exit 1
}

INSTANCE_NAME=$(echo "${STATUS_RESPONSE}" | jq -r '.instanceName')
echo "  Instance:    ${INSTANCE_NAME}"
echo "  Connection:  ✓ OK"

if [ -n "${AGENT_ID}" ]; then
  echo "  Agent ID:    ${AGENT_ID}"

  DEPLOY_CODE=$(curl -s -o /tmp/kemia-status-deploy.json -w "%{http_code}" \
    -H "Authorization: Bearer ${API_KEY}" \
    "${BASE_URL}/api/v1/agents/${AGENT_ID}/deploy")

  if [ "${DEPLOY_CODE}" = "200" ]; then
    SNAP_NAME=$(jq -r '.snapshot.name' /tmp/kemia-status-deploy.json)
    SNAP_FILES=$(jq '.files | length' /tmp/kemia-status-deploy.json)
    echo "  Deploy:      ⚡ READY — '${SNAP_NAME}' (${SNAP_FILES} files)"
    echo ""
    echo "Run /import to apply the pending snapshot."
  else
    echo "  Deploy:      — no pending snapshot"
  fi
  rm -f /tmp/kemia-status-deploy.json
else
  echo "  Agent ID:    (none — run /kemia connect to export)"
fi


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