agentleFS
Sign inSign up

SeekerClaw

sepivip/SeekerClaw/CLAUDE.md

Background research: See docs/internal/RESEARCH.md | Source of truth: See PROJECT.md Always think about user experience. This is the top priority when building SeekerClaw. Every UI decision, feature implementation, and config flow should be designed from the user's perspective. Ask: "Is this intuitive? Will the user lose data? Is switching between options seamless?" When in doubt, prioritize ease of use over technical elegance. SeekerClaw (package: com.seekerclaw.app) is an Android app that turns a Solana Seeker phone into a 24/7 personal AI…

CLAUDE.md317 starsChanged 7 months ago
  • Reads credentials
  • Commits and pushes
# CLAUDE.md — SeekerClaw Project Guide

> **Background research:** See `docs/internal/RESEARCH.md` | **Source of truth:** See `PROJECT.md`

## PROJECT.md — Source of Truth

- Read `PROJECT.md` before any feature work
- After shipping any feature: update **Shipped** section + **Changelog**
- After starting any feature: move it to **In Progress**
- Keep **Limitations** section honest — if it doesn't work, list it
- **One-Liner** and **Elevator Pitch** should always reflect current state
- Update **Stats** periodically (tool count, skill count, lines of code)

## Design Principle: UX First

**Always think about user experience.** This is the top priority when building SeekerClaw. Every UI decision, feature implementation, and config flow should be designed from the user's perspective. Ask: "Is this intuitive? Will the user lose data? Is switching between options seamless?" When in doubt, prioritize ease of use over technical elegance.

## What Is This Project

**SeekerClaw** (package: `com.seekerclaw.app`) is an Android app that turns a Solana Seeker phone into a 24/7 personal AI agent. It embeds a Node.js runtime via `nodejs-mobile` and runs the OpenClaw gateway as a foreground service. Users interact with their agent through Telegram — the app itself is minimal (setup, status, logs, settings).

### Supported Devices

- **Primary:** Solana Seeker (Android 14, Snapdragon 6 Gen 1, 8GB RAM)
- **Secondary:** Any Android 14+ with 4GB+ RAM
- **Note:** OEM-modified ROMs (Xiaomi MIUI, Samsung OneUI) may aggressively kill background services — Seeker's stock Android avoids this.

### Development Phases

- **Phase 1 (PoC):** Mock OpenClaw with a simple Node.js Telegram bot (`grammy`/`telegraf`) that responds to a hardcoded message. Proves Node.js runs on device, Telegram round-trip works.
- **Phase 2 (App Shell):** Replace mock with real OpenClaw gateway bundle. Full setup flow, all screens, watchdog, boot receiver.

## Version Tracking (KEEP UPDATED)

> **When updating OpenClaw or nodejs-mobile, update these version strings in ONE place:**
> **`app/build.gradle.kts`** → the `openclawVersion` / `nodejsVersion` vals at the top of the file.
>
> The app version (`versionName` / `versionCode`) is also in `app/build.gradle.kts`, as
> `appVersionName` / `appVersionCode`. All four are declared once and referenced from
> everywhere else — never retype a value a second time, or the copies drift.
>
> **Identity is read at RUNTIME — never off `BuildConfig` (BAT-1293).**
> UI and diagnostics get `versionName` / `versionCode` from
> `BuildProvenance.installed(context)`, which asks `PackageManager` about the
> **installed** package, and `commit` / `dirty` / `openclawVersion` / `nodejsVersion`
> from `BuildProvenance.get(context)`, which reads the packaged
> `assets/build-metadata.json` (`app/src/main/java/com/seekerclaw/app/config/BuildProvenance.kt`).
>
> A `buildConfigField` for any identity value is FORBIDDEN: it compiles to a
> `static final`, which is INLINED into every reader, so when the value changes
> incremental compilation does not recompile those readers and they keep the OLD
> literal — that is how a build reported a SHA it did not contain. And
> `build-metadata.json` is a build-time COPY, so it is the source for the engine and
> commit fields only, never for `versionName` / `versionCode`.
> `tests/nodejs-project/build-identity-invariant.test.js` enforces both halves in CI.

| Version | Current | Location |
|---------|---------|----------|
| **App** | `2.3.1` (code 25) | `app/build.gradle.kts` → `appVersionName` / `appVersionCode` vals |
| **OpenClaw** | `2026.4.10` | `app/build.gradle.kts` → `openclawVersion` val |
| **Node.js** | `18 LTS` | `app/build.gradle.kts` → `nodejsVersion` val |

## Tech Stack

- **Language:** Kotlin
- **UI:** Jetpack Compose (Material 3, dark theme only)
- **Theme:** `Theme.SeekerClaw`
- **Min SDK:** 34 (Android 14)
- **Node.js Runtime:** nodejs-mobile (https://github.com/nodejs-mobile/nodejs-mobile) — Node 18 LTS, ARM64
- **QR Scanning:** CameraX + ZXing/ML Kit
- **Encryption:** Android Keystore (AES-256-GCM, `userAuthenticationRequired = false`)
- **Background Service:** Foreground Service with `specialUse` type
- **IPC:** nodejs-mobile JNI bridge + localhost HTTP
- **Database:** SQL.js (WASM-compiled SQLite) — no native bindings needed
- **Build:** Gradle (Kotlin DSL)
- **Distribution:** Solana dApp Store APK (primary), Google Play AAB (secondary), direct APK sideload (fallback)

## Project Structure

```
seekerclaw/
├── app/
│   ├── src/main/
│   │   ├── java/com/seekerclaw/app/
│   │   │   ├── MainActivity.kt              # Single activity, Compose navigation
│   │   │   ├── SeekerClawApplication.kt     # App class
│   │   │   ├── ui/
│   │   │   │   ├── theme/Theme.kt            # Dark theme (Theme.SeekerClaw), Material 3
│   │   │   │   ├── navigation/NavGraph.kt    # Setup → Main (Dashboard/Logs/Settings)
│   │   │   │   ├── setup/SetupScreen.kt      # QR scan + manual entry + notification permission
│   │   │   │   ├── dashboard/DashboardScreen.kt
│   │   │   │   ├── logs/LogsScreen.kt        # Monospace scrollable log viewer
│   │   │   │   └── settings/SettingsScreen.kt
│   │   │   ├── service/
│   │   │   │   ├── SeekerClawService.kt      # Foreground Service — starts/manages Node.js
│   │   │   │   ├── NodeBridge.kt             # IPC wrapper for nodejs-mobile
│   │   │   │   └── Watchdog.kt               # Monitors Node.js health, auto-restarts
│   │   │   ├── receiver/
│   │   │   │   └── BootReceiver.kt           # BOOT_COMPLETED → start service
│   │   │   ├── config/
│   │   │   │   ├── ConfigManager.kt          # Read/write config (encrypted + prefs)
│   │   │   │   ├── KeystoreHelper.kt         # Android Keystore encrypt/decrypt
│   │   │   │   └── QrParser.kt               # Parse QR JSON payload
│   │   │   └── util/
│   │   │       ├── LogCollector.kt           # Captures Node.js stdout/stderr
│   │   │       └── ServiceState.kt           # Shared state (StateFlow) for UI
│   │   ├── assets/openclaw/                  # Bundled OpenClaw JS (extracted on first launch)
│   │   ├── res/
│   │   └── AndroidManifest.xml
│   └── build.gradle.kts
├── build.gradle.kts                          # Root build file
├── settings.gradle.kts
├── CLAUDE.md
└── docs/internal/          # Internal docs (audits, tracking, plans)
```

## Architecture

```
┌──────────────────────────────────────────────┐
│          Android App (SeekerClaw)             │
│  ┌─────────────┐    ┌──────────────────────┐ │
│  │  UI Activity │    │  Foreground Service   │ │
│  │  (Compose)   │◄──►│                      │ │
│  │              │ IPC│  ┌──────────────────┐ │ │
│  │ • Dashboard  │    │  │ Node.js Runtime  │ │ │
│  │ • Setup      │    │  │ (nodejs-mobile)  │ │ │
│  │ • Logs       │    │  │ ┌──────────────┐ │ │ │
│  │ • Settings   │    │  │ │  OpenClaw     │ │ │ │
│  └─────────────┘    │  │ │  Gateway      │ │ │ │
│                      │  │ └──────────────┘ │ │ │
│  ┌─────────────┐    │  └──────────────────┘ │ │
│  │ Boot Receiver│────►                       │ │
│  ├─────────────┤    │                        │ │
│  │ Watchdog     │────►  (30s health check)   │ │
│  └─────────────┘    └──────────────────────┘ │
└──────────────────────────────────────────────┘
         │ HTTPS              │ HTTPS
         ▼                    ▼
   api.anthropic.com    api.telegram.org
```

- **Foreground Service** keeps Node.js alive 24/7 with `START_STICKY` and partial wake lock
- **Watchdog** checks heartbeat every 30s, expects pong within 10s, restarts Node.js if unresponsive >60s (2 missed checks)
- **Boot Receiver** auto-starts the service after device reboot (`directBootAware=false` for v1 — starts after first unlock)
- **IPC** uses nodejs-mobile JNI bridge for lifecycle + localhost HTTP for rich API

## Screens (4 total)

1. **Setup** (first launch only) — notification permission request (API 33+), QR scan or manual entry of API key, Telegram bot token, owner ID, model, agent name
2. **Dashboard** (main) — status indicator (green/red/yellow), uptime, start/stop toggle, message stats
3. **Logs** — monospace auto-scrolling view, color-coded (white=info, yellow=warn, red=error)
4. **Settings** — edit config (masked fields), model dropdown, auto-start toggle, battery optimization, danger zone (reset/clear memory), about

**Navigation:** Bottom bar with 3 tabs (Dashboard | Logs | Settings). Setup screen has no bottom bar.

## Design Theme (Dark Only)

Theme name: `Theme.SeekerClaw`

| Token | Value |
|-------|-------|
| Background | `#0D0D0D` |
| Surface / Card | `#1A1A1A` |
| Card border | `#FFFFFF0F` |
| Primary (green) | `#00C805` |
| Error | `#FF4444` |
| Warning | `#FBBF24` |
| Accent (purple) | `#A78BFA` |
| Text primary | `#FFFFFF` at 87% opacity |
| Text secondary | `#FFFFFF` at 50% opacity |

## Key Permissions (AndroidManifest)

```xml
FOREGROUND_SERVICE, FOREGROUND_SERVICE_SPECIAL_USE,
RECEIVE_BOOT_COMPLETED, INTERNET, WAKE_LOCK, CAMERA,
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, POST_NOTIFICATIONS
```

- **`POST_NOTIFICATIONS`:** Required on API 33+. Request at runtime during Setup flow before starting the service.
- **`specialUse` service type:** dApp Store friendly — no justification needed. Google Play requires written justification for `specialUse`. Consider `dataSync` as alternative for Play Store (but note 6-hour time limit on Android 14+). Can be flavor-gated if needed.

## Model List

Available models for the dropdown (using API aliases — auto-resolve to latest snapshot):
- `claude-fable-5` — most powerful, newest tier (Fable 5)
- `claude-opus-4-8` — smartest Opus — **default for the Anthropic provider**
- `claude-opus-4-7` — previous flagship (Opus 4.7)
- `claude-opus-4-6` — older flagship (Opus 4.6) — retained: dropping a registry row silently disables Extended Thinking for users still on it (registry drives `reasoningSupport`); drop only once a retired-models concept exists
- `claude-sonnet-4-6` — balanced, recommended (Sonnet 4.6)
- `claude-haiku-4-5` — fast, cheapest (Haiku 4.5)

Defined in `app/src/main/assets/nodejs-project/model-registry.json` — single source of truth read by both Kotlin (`ModelRegistry` in `Providers.kt`) and Node (`model-catalog.js`). A custom model ID typed in the picker is also supported on Anthropic/OpenAI and survives reconcile (BAT-1032).

Model list can be updated via app update or future remote config.

## MCP Servers (Remote Tools)

Users can add remote MCP (Model Context Protocol) servers in Settings > MCP Servers.
Each server provides additional tools via Streamable HTTP transport (JSON-RPC 2.0).

- Config: `McpServerConfig` in `ConfigManager.kt` (id, name, url, authToken, enabled, rateLimit)
- Client: `app/src/main/assets/nodejs-project/mcp-client.js` (MCPClient + MCPManager)
- Integration: `main.js` merges MCP tools into TOOLS array, routes `mcp__<server>__<tool>` calls
- Security: descriptions sanitized, SHA-256 rug-pull detection, results wrapped as untrusted content
- Rate limiting: 10/min per server (configurable), 50/min global ceiling

## QR Config Payload

Base64-encoded JSON:
```json
{
  "v": 1,
  "anthropic_api_key": "sk-ant-api03-...",
  "telegram_bot_token": "123456789:ABCdefGHI...",
  "telegram_owner_id": "987654321",
  "model": "claude-opus-4-8",
  "agent_name": "MyAgent"
}
```

Sensitive fields encrypted via Android Keystore (AES-256-GCM). Non-sensitive fields (model, agent_name) in SharedPreferences. QR generation web tool at `seekerclaw.xyz/setup` (client-side only, keys never leave the browser).

## OpenClaw Config Generation

On setup completion, generate `config.yaml` in the workspace directory:

```yaml
version: 1
providers:
  anthropic:
    apiKey: "{anthropic_api_key}"
agents:
  main:
    model: "{model}"
    channel: telegram
channels:
  telegram:
    botToken: "{telegram_bot_token}"
    ownerIds:
      - "{telegram_owner_id}"
    polling: true
```

## Workspace Seeding

On first launch, seed the workspace directory with:
- **`SOUL.md`** — a default personality template (basic, friendly agent personality)
- **`MEMORY.md`** — empty file

These are standard OpenClaw workspace files — the agent creates and manages them automatically after first launch.

## Watchdog Timing

- **Check interval:** Every 30 seconds, send heartbeat ping to Node.js
- **Response timeout:** Expect pong within 10 seconds
- **Dead declaration:** After 60 seconds of no response (2 consecutive missed checks), declare Node.js dead and restart
- These values are constants in `Watchdog.kt` — easy to tune later

## Build Priority Order

1. Project setup (Gradle, dependencies, theme)
2. Navigation (4 screens with bottom bar)
3. Setup screen (QR scan + manual entry + notification permission request)
4. Config encryption (KeystoreHelper + ConfigManager)
5. Dashboard screen (status UI with mock data)
6. Settings screen (config display/edit)
7. Foreground Service (basic, without Node.js first)
8. nodejs-mobile integration (get Node.js running)
9. **Phase 1 mock:** Simple Node.js Telegram bot responding to hardcoded message
10. **Phase 2:** Replace mock with real OpenClaw gateway bundle
11. Boot receiver + auto-start
12. Watchdog + crash recovery (30s check / 10s timeout / 60s dead)
13. Logs screen (connect to real Node.js output)
14. Polish & testing

## File System Layout (On Device)

```
/data/data/com.seekerclaw.app/
├── files/
│   ├── nodejs/              # Node.js runtime (bundled in APK)
│   ├── openclaw/            # OpenClaw JS package (bundled, extracted on first launch)
│   ├── workspace/           # OpenClaw working directory (preserved across updates)
│   │   ├── config.yaml
│   │   ├── SOUL.md          # Agent personality (seeded on first launch)
│   │   ├── MEMORY.md        # Long-term memory (empty on first launch)
│   │   ├── memory/          # Daily memory files
│   │   ├── HEARTBEAT.md
│   │   └── node_debug.log   # Node runtime debug log (LEVEL|epochMs|message; ~5MB continuous rotation → node_debug.log.old)
│   └── service_logs         # Kotlin service-log mirror (300-line in-memory ring + 1MB→512KB file compaction)
├── databases/seekerclaw.db
└── shared_prefs/seekerclaw_prefs.xml
```

## Mobile-Specific Config

OpenClaw config overrides for mobile environment:
- Heartbeat interval: 5 min (save battery vs desktop default)
- Memory max daily files: 30 (limit disk usage)
- Debug log (node_debug.log): ~5MB continuous rotation, previous generation kept as .old (size-based, no time retention); service_logs mirror: 300-line ring + 1MB→512KB compaction
- Max context tokens: 100,000 (limit memory usage)
- Web fetch timeout: 15s (shorter for mobile networks)
- Disabled skills: browser, canvas, nodes, screen

## Memory Preservation (CRITICAL)

> **RULE: App updates and code changes MUST NEVER affect user memory.**

The agent's memory is sacred. These files live in the workspace directory and must survive all updates:

| File | Purpose | MUST Preserve |
|------|---------|---------------|
| `SOUL.md` | Agent personality | YES |
| `IDENTITY.md` | Agent name/nature | YES |
| `USER.md` | Owner info | YES |
| `MEMORY.md` | Long-term memory | YES |
| `memory/*.md` | Daily memory files | YES |
| `HEARTBEAT.md` | Last heartbeat | YES |
| `config.yaml` | Config (regenerated) | Regenerated from encrypted store |
| `skills/*.md` | Custom user skills | YES |

### Rules for Developers

1. **Never delete workspace/** during app updates
2. **Never overwrite** existing SOUL.md, MEMORY.md, IDENTITY.md, USER.md
3. **Seed files only if they don't exist** (`if (!file.exists())`)
4. **BOOTSTRAP.md** is the only file the agent itself deletes (after first-run ritual)
5. **Config.yaml** is regenerated from encrypted storage on each service start — this is fine
6. **Use `adb install -r`** (replace) to preserve app data during development
7. **Export/Import** feature exists in Settings for backup/restore

### What Gets Lost and When

| Action | Memory Lost? |
|--------|-------------|
| App update (store) | NO |
| `adb install -r` | NO |
| Uninstall + reinstall | YES (use export first!) |
| "WIPE MEMORY" in Settings | YES (intentional) |
| "RESET CONFIG" in Settings | Config only, memory preserved |
| Factory reset | YES (use export first!) |

---

## Agent Self-Awareness (NEVER SKIP)

> **RULE: When adding or changing features that affect what the agent can do, you MUST update the agent's system prompt and tool descriptions so the agent knows about its own capabilities.**

The agent only knows what we tell it. If we add a new tool, database table, bridge endpoint, or capability but don't update the system prompt or tool descriptions, the agent will tell users "I can't do that" — even though it can.

### What to Update

| Change | Update Required |
|--------|----------------|
| New tool added to TOOLS array | Tool `description` must explain what it does and what data it accesses |
| New bridge endpoint | Add to `buildSystemBlocks()` Android Bridge section |
| New database table or query | Mention in relevant tool descriptions + `buildSystemBlocks()` Data & Analytics section |
| Changed tool behavior | Update tool `description` to reflect new behavior |
| New system capability | Add to `buildSystemBlocks()` in the appropriate section |

### Where to Update (in `main.js`)

1. **Tool descriptions** — `TOOLS` array (each tool has a `description` field). Be specific: say "SQL.js database" not "search files", say "API usage analytics" not "stats".
2. **System prompt** — `buildSystemBlocks()` function. Sections include: Identity, Tooling, Skills, Memory Recall, Data & Analytics, Android Bridge, Runtime info, etc.

### Example

Bad: Adding `memory_search` tool with description "Search memory files"
Good: Adding `memory_search` tool with description "Search your SQL.js database (seekerclaw.db) for memory content. All memory files are indexed into searchable chunks — this performs ranked keyword search with recency weighting, returning top matches with file paths and line numbers."

### SAB Audit BEFORE Merge (NEVER SKIP)

> **RULE: If a PR touches `buildSystemBlocks()` in `ai.js`, modifies `DIAGNOSTICS.md`, adds new error log sites in JS, or ships any user-visible AI capability — run an SAB audit BEFORE merging, not after.**

The Self-Awareness Benchmark (SAB) catches drift between what the agent can do and what the agent knows it can do. SAB v3's behavioral probes are particularly good at catching gaps invisible to a human reviewer ("the auth flow is implementation detail, the agent doesn't need to know" → wrong, the user will ask).

**When to run SAB before merge:**
- New feature shipped (any provider, channel, tool, skill type, auth flow, etc.)
- New error log sites added in JS (`log(...'ERROR'...)` or `log(...'WARN'...)`)
- `buildSystemBlocks()` itself touched
- `DIAGNOSTICS.md` touched
- Any new user-facing capability

**How:**
1. Run the `sab-audit` skill (or invoke `/sab-audit`)
2. Score honestly — pre-fix score below 95% means drift
3. Apply the gap fixes IN THE SAME PR (don't ship the feature with a follow-up "audit fix" PR — that means the feature shipped broken)
4. Verify post-fix score is 100%
5. Reference the SAB audit version in the PR description

**Discovered the hard way in PR #316 (BAT-485, OAuth):** the feature shipped functionally correct but with zero self-knowledge coverage. SAB-AUDIT-v19 caught 5 gaps after merge — they should have been caught before. Don't repeat.

**Repeated in PR #304 (BAT-500, Activity Heatmap):** SAB-AUDIT-v23 caught 3 failed behavioral probes post-merge; pre-fix 93.6%, lowest since v19.

**Enforcement:** `.github/PULL_REQUEST_TEMPLATE.md` has a **Self-Awareness Checklist** — check exactly one of "N/A" or "SAB-audited" on every PR. Unchecked = unreviewable. This is the honor-system gate version (BAT-503); if drift keeps happening, graduate to a CI trailer check.

---

## Telegram Slash Command Registration (NEVER SKIP)

> **RULE: When you add a new Telegram slash command, add an entry to `telegram-commands.js` AND a `case '/foo':` branch in `message-handler.js`. The drift-guard test fails the build if either half is missing.**

Handling a command is half the work. The other half is making it discoverable. The single-source-of-truth registry in [telegram-commands.js](app/src/main/assets/nodejs-project/telegram-commands.js) drives both the BotFather `/` autocomplete menu AND the `/help` response, so one edit serves both surfaces.

### What to do

1. **Registry entry** — add one line to `COMMAND_REGISTRY` in [telegram-commands.js](app/src/main/assets/nodejs-project/telegram-commands.js). Order in the array determines display order in both `/` autocomplete and `/help`.
   ```js
   { name: 'foo', description: 'Short imperative description', fallback: true },
   ```
   Set `fallback: true` if the command should survive a Telegram `BOT_COMMANDS_TOO_MUCH` degradation (i.e., it's a must-have). Keep the fallback list short.

2. **Handler** — new `case '/foo':` branch in `handleCommand` in [message-handler.js](app/src/main/assets/nodejs-project/message-handler.js).

Description should be short (fits in Telegram's one-line UI), imperative ("Show bot status", not "Shows bot status"), and mention the affected thing so users can distinguish similar commands.

### Budget

Telegram caps at ~100 total commands per bot but we cap OURSELVES at ~30 for usability. If adding a new one would push past that, merge two existing ones or prune before adding. A command menu that scrolls forever is no better than no menu.

**If BotFather rejects with `BOT_COMMANDS_TOO_MUCH`:** the fallback payload kicks in automatically (only commands flagged `fallback: true`). Keep the full payload trimmed so we never rely on the fallback in production.

### Enforcement

[tests/nodejs-project/telegram-commands.test.js](tests/nodejs-project/telegram-commands.test.js) runs a **drift-guard** that fails the build if:
- a command in `COMMAND_REGISTRY` has no matching `case '/<name>':` in message-handler.js, OR
- a `case '/<name>':` in message-handler.js has no matching registry entry (internal aliases like `/commands` → `/help` handler stack on the same case block — fine).

Run locally with `node tests/nodejs-project/telegram-commands.test.js` before pushing.

### Discovered the hard way in PR #339 (BAT-504, /model + /provider)

The `/model` and `/provider` commands shipped functional but invisible — device testing found that typing `/` in Telegram didn't surface them in the autocomplete menu, so a user who didn't already know the command existed wouldn't find it. PR #339 also refactored the old hardcoded setMyCommands + inline /help text into the registry pattern + drift-guard so a human can't make the same mistake again.

---

## Key Implementation Details

- **nodejs-mobile:** https://github.com/nodejs-mobile/nodejs-mobile — Node 18 LTS, ARM64. Adapted from React Native integration guide for pure Kotlin.
- **nodejs-mobile JNI architecture (IMPORTANT):** Node.js runs as `libnode.so` loaded via `System.loadLibrary("node")` through JNI — there is **NO standalone `node` binary** on the device. Key implications:
  - `process.execPath` typically points to Android's app process launcher (e.g., `/system/bin/app_process` or `/system/bin/app_process64`), **not** a Node.js binary path
  - `process.env.PATH` primarily contains Android system directories (e.g., `/system/bin`, `/vendor/bin`)
  - `node`, `npm`, `npx` commands **cannot** be found or executed via `shell_exec` / `child_process`
  - `shell_exec` uses Android's `/system/bin/sh` (toybox) — completely separate from the Node.js process
  - To run JavaScript code, tools must use `eval()`/`require()` inside the existing Node.js process (`js_eval` tool)
  - All existing tools (read, write, web_fetch, etc.) already work within the Node.js process — they don't shell out
- **Phase 1 mock:** Create `assets/openclaw/` with `package.json` and `index.js` that starts a Telegram bot (`grammy`/`telegraf`), responds to a hardcoded message from the owner, and sends heartbeat pings back to the Android bridge.
- **Phase 2 real:** Replace mock with actual OpenClaw gateway bundle. Config, workspace, and all features work as documented.
- **Logs:** Node writes `node_debug.log` (workspace/) via `log()` in `LEVEL|epochMs|message` format, continuously rotated at ~5MB (previous kept as `.old`, no carryover). A Kotlin FileObserver forwarder (SeekerClawService) mirrors new lines into `service_logs` (filesDir) — a 300-line in-memory ring surfaced in the Logs screen, plus a 1MB→512KB compacted file. `service_logs` is a bounded diagnostic mirror, NOT an authoritative replica; `node_debug.log` is the full Node transcript.
- **Battery:** On first launch after setup, show dialog explaining battery optimization exemption, then call `Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`.
- **ServiceState:** Singleton with `StateFlow<ServiceStatus>` (STOPPED, STARTING, RUNNING, ERROR), uptime, and message count. UI observes these flows.
- **Metrics:** Message count, uptime, and response times tracked locally on-device. Usage analytics (Firebase) tracks feature usage, service health, and model selection — no personal data, no messages, no wallet keys. Fully optional — users can disable in Settings.

## Jupiter / Solana Integration (Live-tested 2026-02-22)

Full Jupiter DEX integration via MWA (Mobile Wallet Adapter). Tested on Solana Seeker with real funds.

**Capabilities:** wallet connect, balance, quotes, swaps (Jupiter Ultra — gasless), SOL/SPL transfers, token search, price lookup, security checks, holdings

**Safety layers (all verified):**
- Two-step confirmation gate (quote → YES/NO prompt → 60s auto-cancel)
- Balance pre-check blocks insufficient-funds swaps before wallet popup
- Rate limiting (15s cooldown on swap/send)
- MWA sign-only mode (no private keys in app)
- Clean error handling on wallet rejection, timeout, network loss

**Test docs:** `docs/internal/audits/JUPITER-AUDIT.md` (code audit), `docs/internal/audits/JUPITER-TEST-CHECKLIST.md` (29 tests, all must-pass green)

## What NOT to Build (v1)

- No in-app chat (users use Telegram)
- No light theme
- No multi-agent support
- No OTA updates (update via app store)
- No multi-channel (Telegram only)

## Product Flavors (Distribution)

Two product flavors under the `distribution` dimension, defined in `app/build.gradle.kts`:

| Flavor | Output | Signing Config | Use |
|--------|--------|---------------|-----|
| `dappStore` | APK | `dappStore` (SEEKERCLAW_* keys) | Solana dApp Store + sideload |
| `googlePlay` | AAB | `googlePlay` (PLAY_* keys) | Google Play Store |

**BuildConfig fields** available in Kotlin code:
- `BuildConfig.DISTRIBUTION` — `"dappStore"` or `"googlePlay"`
- `BuildConfig.STORE_NAME` — `"Solana dApp Store"` or `"Google Play"`

These two are flavor selectors, fixed per variant. **No build-identity value belongs
here** — no version, SHA, or build time; a constant inlines into its readers and goes
stale on an incremental build. See *Version Tracking* above.

**Signing config resolution:** `signingProp(localKey, envKey)` helper checks `local.properties` first (Android Studio), then `System.getenv()` (GitHub Actions CI).

**Build variants** (flavor + buildType):
- `dappStoreDebug`, `dappStoreRelease`
- `googlePlayDebug`, `googlePlayRelease`

**Android Studio:** Select build variant in Build Variants panel (default: `dappStoreDebug`).

**Future:** Flavors enable feature stripping per store (e.g., removing Solana wallet from Google Play version).

## Build & Run

```bash
# dApp Store debug (default for development)
./gradlew assembleDappStoreDebug
adb install app/build/outputs/apk/dappStore/debug/app-dappStore-debug.apk

# Google Play debug
./gradlew assembleGooglePlayDebug

# Release builds (require signing keys)
./gradlew assembleDappStoreRelease    # → APK
./gradlew bundleGooglePlayRelease     # → AAB
```

### Pre-push check (BAT-502)

Before every `git push`, run:

```bash
scripts/pre-push-check.sh
```

Does in ~5–10 seconds (incremental):
1. Node.js smoke test (`tests/nodejs-project/smoke.js`) — syntax + module load
2. Kotlin compile (`compileDappStoreDebugKotlin`) — catches missing imports,
   type errors, unresolved references BEFORE CI

The script auto-detects JDK 17+ (prefers Android Studio's bundled JBR at
`jbr/`) and Android SDK (from main-repo `local.properties` or standard
locations).

Exit codes:
- `0` — all checks passed
- `1` — Node smoke failed
- `2` — Kotlin compile failed
- `3` — JDK 17+ not found
- `4` — Android SDK not found
- `5` — couldn't cd to repo root (broken path / permissions)

Catches the bug class from PR #334 (missing `aspectRatio` import passed
Node smoke, failed CI after 4 minutes).

## CI/CD (GitHub Actions)

**`.github/workflows/build.yml`** — runs on push/PR to main, builds both flavor debug APKs for validation.

**`.github/workflows/release.yml`** — triggered by `v*` tags, 3 parallel jobs:
1. `build-dappstore` — signed APK (`assembleDappStoreRelease`), renamed to `SeekerClaw-{tag}.apk`
2. `build-googleplay` — signed AAB (`bundleGooglePlayRelease`), renamed to `SeekerClaw-{tag}.aab`
3. `release` — downloads both artifacts, extracts changelog, creates GitHub Release

**GitHub Secrets:**

| Secret | Purpose |
|--------|---------|
| `KEYSTORE_BASE64` | Base64-encoded dApp Store .jks |
| `STORE_PASSWORD` | dApp Store keystore password |
| `KEY_ALIAS` | dApp Store key alias |
| `KEY_PASSWORD` | dApp Store key password |
| `PLAY_KEYSTORE_BASE64` | Base64-encoded Google Play .jks |
| `PLAY_STORE_PASSWORD` | Google Play keystore password |
| `PLAY_KEY_ALIAS` | Google Play key alias |
| `PLAY_KEY_PASSWORD` | Google Play key password |
| `GOOGLE_SERVICES_JSON` | Base64-encoded Firebase config |

**Re-releasing:** To rebuild artifacts for an existing tag, delete and recreate the tag:
```bash
git tag -d v1.x.x && git push origin :refs/tags/v1.x.x
git tag v1.x.x && git push origin v1.x.x
```

**Release Candidate (RC) flow:** Before submitting to dApp Store, always test the signed APK:
1. Tag `v1.x.x-rc1` → triggers release workflow → creates **pre-release** on GitHub
2. Download APK from GitHub Releases, install on Seeker, verify it launches and works
3. If good → tag `v1.x.x` (final release), submit to dApp Store
4. If bad → fix, tag `v1.x.x-rc2`, repeat

## Reference Documents

- `docs/internal/RESEARCH.md` — Deep feasibility research on Node.js on Android, background services, Solana Mobile, competitive landscape
- `docs/internal/OPENCLAW_TRACKING.md` — **Critical:** Version tracking, change detection, and update process

---

## OpenClaw Version Tracking

> **IMPORTANT:** SeekerClaw must stay in sync with OpenClaw updates. See `docs/internal/OPENCLAW_TRACKING.md` for full details.

### Current Versions
- **OpenClaw Reference:** 2026.4.10 (parity baseline; upstream has since diverged ~34K commits — now a loose reference)
- **Last Sync Review:** 2026-07-06 (reviewed HEAD 538d4eeb77; nothing portable — see `docs/internal/OPENCLAW_TRACKING.md`)

### Quick Update Check
```bash
# Check for new OpenClaw versions
cd openclaw-reference && git fetch origin
git log --oneline HEAD..origin/main

# If updates exist, pull and review
git pull origin main
# Then review docs/internal/OPENCLAW_TRACKING.md for what to check
```

### When OpenClaw Updates

1. **Pull the update:** `cd openclaw-reference && git pull`
2. **Check critical files:** See priority list in `docs/internal/OPENCLAW_TRACKING.md`
3. **Compare changes:** `git diff <old>..<new> -- <file>`
4. **Port relevant changes** to `main.js` and skills
5. **Update tracking docs** with new version info

### Files That Require Immediate Review
- `src/agents/system-prompt.ts` — System prompt changes
- `src/memory/` — Memory system changes
- `src/cron/` — Scheduling changes
- `skills/` — New or updated skills

---

## OpenClaw Compatibility

> **Goal:** SeekerClaw should behave as close to OpenClaw as possible.

### Reference Repository

OpenClaw source is cloned at `openclaw-reference/` for direct comparison.

```bash
# Update OpenClaw reference
cd openclaw-reference && git pull
```

### Key OpenClaw Files to Monitor

| OpenClaw File | Purpose | SeekerClaw Equivalent |
|---------------|---------|----------------------|
| `src/agents/system-prompt.ts` | System prompt builder | `ai.js:buildSystemBlocks()` |
| `src/skills/loading/` + `src/skills/discovery/` | Skills loading | `skills.js:loadSkills()` |
| `src/memory/` | Memory management | `memory.js` (simplified) |
| `src/cron/service/` + `src/cron/types.ts` | Cron/scheduling | `cron.js:cronService` (ported) |
| `skills/` | 76 bundled skills | `workspace/skills/` (3 examples) |

### OpenClaw Compatibility Checklist

**System Prompt Sections:**
- [x] Identity line
- [x] Tooling section
- [x] Tool Call Style
- [x] Safety section (exact copy)
- [x] Skills section
- [x] Memory Recall
- [x] Workspace
- [x] Project Context (SOUL.md, MEMORY.md)
- [x] Heartbeats
- [x] Runtime info
- [x] Silent Replies (SILENT_REPLY token)
- [x] Reply Tags ([[reply_to_current]])
- [x] User Identity

**Memory System:**
- [x] MEMORY.md
- [x] Daily memory files (memory/*.md)
- [x] HEARTBEAT.md
- [ ] Vector search (requires Node 22+)
- [ ] FTS search
- [ ] Line citations

**Skills System:**
- [x] SKILL.md loading
- [x] Trigger keywords
- [x] YAML frontmatter format
- [x] Semantic triggering (AI picks skills)
- [ ] Requirements gating (bins, env, config)

**Cron/Scheduling (ported from OpenClaw):**
- [x] cron_create tool (one-shot + recurring)
- [x] cron_list, cron_cancel, cron_status tools
- [x] Natural language time parsing ("in X min", "every X hours", "tomorrow at 9am")
- [x] JSON file persistence with atomic writes + .bak backup
- [x] JSONL execution history per job
- [x] Timer-based delivery (no polling)
- [x] Zombie detection (2hr threshold)
- [x] Recurring intervals ("every" schedule)
- [x] HEARTBEAT_OK protocol

### SKILL.md Format

> **Full spec:** See `SKILL-FORMAT.md` — Claude uses this as the reference when creating skills.

**SeekerClaw Format (current):**
```yaml
---
name: skill-name
description: "What the skill does — AI reads this to decide when to use"
version: "1.0.0"
emoji: "🔧"
image: "https://seekerclaw.xyz/assets/partner-skills/skill-name.jpg"
requires:
  bins: []
  env: []
allowed-tools:
  - tool1
  - tool2
---

# Skill Name

Instructions...
```

**Key fields:**
- `image:` — Absolute HTTPS URL to skill logo. Displayed in Skills screen via Coil. Falls back to emoji → ⚡. **Partner skills** host images at `https://seekerclaw.xyz/assets/partner-skills/{skill-id}.{ext}`, with image files in the `SeekerClaw_Web` repo under `assets/partner-skills/`.
- `allowed-tools:` — Restricts which tools the skill can use (important for partner skills).
- OpenClaw's nested `metadata.openclaw` format is also supported; top-level fields take precedence.

**Legacy format** (`Trigger: keyword1, keyword2`) is deprecated — still parsed but logs warnings.

### SOUL.md Template

SeekerClaw uses the **exact same SOUL.md template** as OpenClaw:

```markdown
# SOUL.md - Who You Are

_You're not a chatbot. You're becoming someone._

## Core Truths
- Be genuinely helpful, not performatively helpful
- Have opinions
- Be resourceful before asking
- Earn trust through competence
- Remember you're a guest
...
```

### Node.js Limitations

OpenClaw requires **Node 22+** for `node:sqlite`. SeekerClaw runs on **Node 18** (nodejs-mobile limitation).

**Solved:**
- SQLite — uses **SQL.js** (WASM-compiled SQLite, v1.12.0) instead of `node:sqlite`. Bundled as `sql-wasm.js` + `sql-wasm.wasm` in assets. Currently used for API request logging (`api_request_log` table); future: conversation storage, FTS5 memory search.

**Cannot implement (yet):**
- Vector embeddings for semantic search (needs native bindings)

**Current workarounds:**
- File-based memory (MEMORY.md, daily files) — future: migrate to SQL.js
- Keyword matching for skills
- Full file reads for memory recall — future: FTS5 via SQL.js

---

## Android Bridge (Phase 4)

SeekerClaw extends OpenClaw with Android-native capabilities via a local HTTP bridge.

### Architecture
```
Node.js (main.js)  ──HTTP POST──►  AndroidBridge.kt (port 8765)  ──►  Android APIs
```

### Available Endpoints

| Endpoint | Purpose | Permission Required |
|----------|---------|---------------------|
| `/battery` | Battery level, charging status | None |
| `/storage` | Storage stats | None |
| `/network` | Network connectivity | None |
| `/clipboard/get` | Read clipboard | None |
| `/clipboard/set` | Write clipboard | None |
| `/contacts/search` | Search contacts | READ_CONTACTS |
| `/contacts/add` | Add contact | WRITE_CONTACTS |
| `/sms` | Send SMS | SEND_SMS |
| `/call` | Make phone call | CALL_PHONE |
| `/location` | Get GPS location | ACCESS_FINE_LOCATION |
| `/tts` | Text-to-speech | None |
| `/apps/list` | List installed apps | None |
| `/apps/launch` | Launch app | None |
| `/stats/message` | Report message for stats | None |
| `/ping` | Health check | None |

### Using from Node.js
```javascript
async function androidBridgeCall(endpoint, data = {}) {
    const http = require('http');
    return new Promise((resolve) => {
        const req = http.request({
            hostname: 'localhost',
            port: 8765,
            path: endpoint,
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
        }, (res) => {
            let body = '';
            res.on('data', chunk => body += chunk);
            res.on('end', () => resolve(JSON.parse(body)));
        });
        req.write(JSON.stringify(data));
        req.end();
    });
}

// Example: Get battery level
const battery = await androidBridgeCall('/battery');
// Returns: { level: 85, isCharging: true, chargeType: "usb" }
```

---

## Theme

SeekerClaw uses a single **DarkOps** theme (dark navy + crimson red + green status). Colors are defined in `Theme.kt` via `DarkOpsThemeColors` and accessed globally through the `SeekerClawColors` object.

---

## Coding Patterns & Pitfalls

Hard-won lessons from code review. Follow these patterns to avoid recurring bugs.

### ProGuard / R8 and @Serializable

All `@Serializable` classes in `com.seekerclaw.app.**` are protected by wildcard rules in `proguard-rules.pro`. Adding new `@Serializable` objects (routes, data classes) requires no extra steps. If you move serializable classes outside this package, add a matching keep rule.

### Timer Cleanup

**Always track setTimeout IDs and clear them.** Dangling timers cause stale callbacks, memory leaks, and ghost state changes.

```javascript
// BAD — fire-and-forget timer, no way to cancel
setTimeout(() => clearReaction(), 1500);

// GOOD — track and clean up
holdTimer = setTimeout(() => clearReaction(), 1500);
// In dispose/cleanup:
if (holdTimer) clearTimeout(holdTimer);
```

Applies to: `Promise.race` timeouts, hold/delay timers, debounce timers, stall timers.

### Early Return Cleanup

**Every early `return` in an async handler must clean up state.** If a handler creates stateful resources (reactions, locks, pending operations), every exit path must release them.

```javascript
// BAD — statusReaction left as 👀 forever
if (skillAutoInstalled && !text) {
    return;
}

// GOOD — clean up before early return
if (skillAutoInstalled && !text) {
    await statusReaction.clear();
    return;
}
```

When adding a new early return to `handleMessage()` in `main.js`, always check: "Is there a `statusReaction` that needs clearing?"

### Serialize Async State Updates

**When multiple async calls update the same state, serialize them.** Fire-and-forget async calls can complete out of order, causing later states to be overwritten by earlier slow responses.

```javascript
// BAD — overlapping API calls can resolve out of order
async function setReaction(emoji) {
    currentEmoji = emoji;
    await telegram('setMessageReaction', { reaction: [{ type: 'emoji', emoji }] });
}

// GOOD — promise chain ensures sequential execution
let chain = Promise.resolve();
async function setReaction(emoji) {
    chain = chain.then(async () => {
        await telegram('setMessageReaction', { reaction: [{ type: 'emoji', emoji }] });
        currentEmoji = emoji; // Only update after success
    });
    return chain;
}
```

### Defensive Field Validation

**Guard every field from persisted JSON.** Cron jobs, configs, and any data loaded from files can be corrupt (NaN, null, wrong type). Validate before arithmetic.

```javascript
// BAD — trusts persisted data
const anchor = schedule.anchorMs || 0;

// GOOD — validates type and finiteness
const anchor = (typeof schedule.anchorMs === 'number' && isFinite(schedule.anchorMs))
    ? schedule.anchorMs : 0;
```

### Consistent JSON Output

**Use `?? null` for optional fields in tool results.** `undefined` is silently dropped by `JSON.stringify`, making output shape inconsistent. Tools should return stable schemas.

```javascript
// BAD — field disappears from JSON when undefined
lastDelivered: j.state.lastDelivered,

// GOOD — explicit null keeps field in output
lastDelivered: j.state.lastDelivered ?? null,
```

### Bootstrap / Multi-Step Ritual Guards

**For multi-step rituals, gate on the trigger file only.** The trigger file (BOOTSTRAP.md) is the source of truth for "ritual in progress." The agent deletes it when done. If the result file (IDENTITY.md) already exists alongside the trigger, treat it as crash recovery — inject resume context, don't skip.

```javascript
// BAD — kills multi-step ritual if agent writes partial results mid-way
if (bootstrap && !identity) { runRitual(); }

// GOOD — trigger file is sole source of truth; add resume note if identity exists
if (bootstrap) { runRitual(/* resume: !!identity */); }
```

### SSE Streaming — Handle EVERY Delta Type (thinking signatures)

**When assembling a streamed API response, handle every `*_delta` type the wire can send — a dropped delta silently corrupts the block.** The Claude SSE reducer in `http.js` (`applyClaudeStreamEvent`) accumulates `text_delta`, `input_json_delta`, `thinking_delta`, and `signature_delta`. Anthropic streams a thinking block as an empty shell followed by deltas:

```text
content_block_start  { type:'thinking', thinking:'', signature:'' }
content_block_delta  { thinking_delta:  <reasoning text> }
content_block_delta  { signature_delta: <the signature> }   ← easy to forget
```

**The v2.1.0 bug (BAT-1033):** the reducer handled only `text_delta` + `input_json_delta`, so it dropped `signature_delta`. The assembled thinking block kept the empty signature from `content_block_start`. On the next tool-loop round the block was echoed back (Anthropic requires thinking blocks to be replayed **unchanged** on tool-use turns) and the API rejected the whole request:

```text
API error (400): messages.N.content.0.thinking: each thinking block must contain thinking
```

**The message is misleading — the trigger is the empty SIGNATURE, not empty text.** Proven with a live probe (`tests/live/anthropic/test-thinking-poison.js`): a signed *empty-text* thinking block replays 200; the *same* block with `signature:''` replays 400. So:

- **Capture side:** never lose the signature. `signature_delta` MUST be accumulated.
- **Replay guard (`claude.js:_collectClaudeWireBlocks`):** skip a thinking block whose `signature` is empty/whitespace — **key on the signature, not the text** (an empty-text signed block is valid and must still replay). This also recovers checkpoints poisoned by an older build after upgrade.
- **Why only Sonnet 5 surfaced it:** with `/think` off, Sonnet 5 emits a thinking block by default while Opus 4.8 doesn't — and emission is stochastic, which is why it flapped ("works now" → "broke again"). Any model that emits a thinking block hits it on the first tool-using turn.

### Adaptive Thinking — `budget_tokens` was removed (verify per auth path)

**A second, distinct BAT-1033 bug:** with `/think` **ON**, `formatRequest` used to send `thinking:{type:'enabled', budget_tokens}` (extended thinking). Anthropic **removed** extended thinking from the current models — fable-5/opus-4-8/opus-4-7/sonnet-5 reject it with `400 "thinking.type.enabled is not supported for this model. Use thinking.type.adaptive"`. Fix: send `thinking:{type:'adaptive'}` uniformly (the model auto-sizes its budget; accepted by every reasoning model). This retired the BAT-558 budget clamp (no `budget_tokens` → no `budget_tokens < max_tokens` constraint).

**The trap that nearly made us defer it — auth path matters.** The `cc_version` billing masquerade on the **setup_token** path still *tolerated* the deprecated `budget_tokens` (returned 200), so probing only that path made it look "latent." On the **raw API-key** path (the common dApp-Store user, QR `anthropic_api_key`), the same request **400s**. **Always verify a provider-shape claim on BOTH auth paths** — `tests/live/anthropic/test-thinking-matrix.js` runs the per-model × extended/adaptive grid on the api-key path; `test-thinking-repro.js` covers setup_token.

### Wire-Contract Bugs Need Live Probes, Not Just Mocks

**A fully-mocked test cannot catch a bug in what the *live API* actually accepts.** BAT-1033 shipped even though `claude-reasoning-roundtrip.test.js` existed — its mocks only covered *missing*-signature/wrong-type blocks, never the *empty-string* signature the real stream produces. Two-layer defense:

1. **Offline (CI/smoke):** extract wire-assembly into a pure exported reducer and feed it the EXACT bytes the API streams (`tests/nodejs-project/claude-thinking-signature.test.js` drives `http.js`'s real `assembleClaudeStreamMessage`). The test must fail if the fix is reverted — verify that.
2. **Live (opt-in, `tests/live/anthropic/`):** `test-thinking-repro.js` (per-model × reasoning on/off/extended/adaptive matrix) and `test-thinking-poison.js` (verbatim vs signature-stripped replay) hit the real endpoint with a setup_token. Run before any release that touches provider request/response shaping.

### Verify External API Contracts via context7 (not training memory)

**Before writing or changing any provider request/response shaping — Anthropic, OpenAI, OpenRouter — pull the CURRENT API contract via the context7 MCP first; do not rely on training memory.** Model APIs change faster than the training cutoff. In BAT-1033, context7 (`platform.claude.com`) is what surfaced that Anthropic **removed** `thinking:{type:'enabled', budget_tokens}` from the current models and requires `type:'adaptive'` — a fact no amount of code-reading or memory would have revealed, and which the fix hinged on. Pair it with a live probe (above): context7 tells you the documented contract, the probe tells you what the endpoint (and our specific auth path) actually enforces. Applies to any hardcoded external identifier — API params, model ids, header/beta tags, endpoint paths.

Discussion

Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.

Posts are public.Sign in to post

No one has posted yet. Be the first.