Skill: junwei1213/openclaw-mention-skill (version 1.0.1)

--- name: whatsapp-mention slug: whatsapp-mention description: Model-agnostic WhatsApp @mention skill for OpenClaw. Converts @Name, @Phone, @LID to blue clickable WhatsApp mentions with any AI model. version: 1.0.2 author: InstallMyClaw license: MIT repository: https://github.com/junwei1213/openclaw-mention-skill tags: - whatsapp - mention - baileys - group metadata: openclaw: requires: bins: - node - bash - python3 channels: - whatsapp --- # WhatsApp @Mention Skill Make WhatsApp @mentions work reliably with **any AI model** in OpenClaw. ## What it does Intercepts all outgoing WhatsApp messages and automatically converts mentions to the correct format: - `@Name` → `@LID` (blue clickable mention) - `@PhoneNumber` → `@LID` (blue clickable mention) - `@LID` → kept as-is (already correct) ## Installation ```bash openclaw skills install whatsapp-mention ``` Or manually: ```bash git clone https://github.com/junwei1213/openclaw-mention-skill.git cd openclaw-mention-skill bash install.sh ``` ## How to use Just write `@someone` in your messages. The skill handles the rest. The LID cache at `/home/openclaw/.openclaw/workspace/LID_CACHE.json` maps names to WhatsApp LIDs. Members are auto-discovered when they send messages in groups. ### Add a member manually ```bash node add-member.js --lid 123456789012345 --name "John" --phone 1234567890 ``` ### Find a missing LID Check Baileys' LID mapping files in your WhatsApp credential directory. Filenames follow the pattern `lid-mapping-<LID>_reverse.json`. ## Re-install after OpenClaw updates OpenClaw updates overwrite patched files. Re-run `install.sh` after updating. ## Uninstall ```bash bash uninstall.sh ```

path: README.md
content: # OpenClaw WhatsApp @Mention Skill

Make WhatsApp @mentions work reliably with **any AI model** in [OpenClaw](https://openclaw.ai).

## The Problem

When OpenClaw bots try to @mention group members in WhatsApp:
- Different AI models output mentions differently (`@John`, `@1234567890`, `@123456789012345`)
- WhatsApp requires a specific format: `@LID` in text + `LID@lid` in the `mentions` array
- Switching models breaks mentions because each model formats them differently

## The Solution

A **model-agnostic patch** that intercepts all outgoing messages and automatically:

1. Converts `@Name` → `@LID` (using a local LID cache)
2. Converts `@PhoneNumber` → `@LID`  
3. Keeps `@LID` as-is
4. Builds the correct Baileys `mentions` array with `LID@lid` format

The AI model just needs to write `@Someone` — the patch handles the rest.

## How It Works

```
AI outputs: "Hello @John @Jane Doe!"
                    ↓ patch intercepts
LID Cache lookup: John → 123456789012345
                    ↓
Sent to WhatsApp: text="Hello @123456789012345 @987654321098765!"
                  mentions=["123456789012345@lid", "987654321098765@lid"]
                    ↓
WhatsApp renders: Blue clickable @mentions ✅
```

## Installation

### Via ClawHub (recommended)

```bash
openclaw skills install whatsapp-mention
```

### Manual Install

```bash
# 1. Clone and review the code first
git clone https://github.com/junwei1213/openclaw-mention-skill.git
cd openclaw-mention-skill

# 2. Review install.sh before running — it patches OpenClaw core files
cat install.sh

# 3. Run the installer
bash install.sh
```

> **Note:** This skill patches OpenClaw's `deliver-reply` and `login` files. Backups are created automatically before patching. Run `bash uninstall.sh` to restore originals.

## What Gets Installed

| File | Purpose |
|------|---------|
| `deliver-reply` patch | Intercepts outgoing messages, converts @mentions |
| `login` patch | Makes `reply()` accept `{text, mentions}` objects |
| `LID_CACHE.json` | Maps names/phones/aliases to WhatsApp LIDs |
| `mention-guide.md` | AI memory file with LID lookup table |

## Configuration

### LID Cache

The skill auto-discovers group member LIDs when messages are received. You can also manually add members:

```bash
# View current cache
cat /home/openclaw/.openclaw/workspace/LID_CACHE.json | python3 -m json.tool

# Add a member manually
node add-member.js --lid 123456789012345 --name "John" --phone 1234567890 --group "[REDACTED_SECRET]"
```

### Finding a Member's LID

If a member's LID isn't auto-discovered, check Baileys' LID mapping files in your WhatsApp credential directory. The filename pattern is `lid-mapping-<LID>_reverse.json` and contains the phone number.

### AI Memory

The installer creates `mention-guide.md` in the bot's memory directory. Edit it to add/update member LIDs:

```bash
nano /home/openclaw/.openclaw/workspace/memory/mention-guide.md
```

## Compatibility

- **OpenClaw**: 2026.3.x+
- **Baileys**: Built-in (bundled with OpenClaw)
- **AI Models**: Works with ALL models (Claude, GPT, Qwen, Gemma, etc.)
- **Node.js**: 18+

## Important Notes

- **OpenClaw updates will overwrite patches.** Re-run `install.sh` after updating OpenClaw.
- The `const chunk` in OpenClaw's delivery loop is automatically fixed to `let chunk` by the installer.
- LIDs are WhatsApp's internal user identifiers (not phone numbers). They look like `123456789012345`.

## Technical Details

### Why LID format?

WhatsApp's mention system requires:
1. Text contains `@IDENTIFIER` where IDENTIFIER matches a JID prefix
2. `mentions` array contains full JIDs (`IDENTIFIER@lid` or `[REDACTED_SECRET]`)

Only `@LID_NUMBER` in text + `LID@lid` in mentions produces **blue clickable mentions**. Using `@Name` in text does NOT work even with correct mentions array.

### Patch Architecture

```
deliverWebReply()
  └─ for each text chunk:
      └─ _LID_PATCH_V10:
          ├─ @name → lookup _names/_aliases → @LID + add to mentions
          ├─ @phone → lookup phone→LID map → @LID + add to mentions  
          ├─ @LID (already) → add to mentions
          └─ msg.reply({ text, mentions }) → sendTrackedMessage → Baileys
```

## License

MIT

## Credits

Built for [OpenClaw](https://openclaw.ai) by [InstallMyClaw](https://installmyclaw.com).


path: add-member.js
content: #!/usr/bin/env node
/**
 * Add a member to the LID_CACHE manually
 * Usage: node add-member.js --lid <LID> --name <Name> [--phone <Phone>] [--group <GroupJID>] [--alias <Alias>...]
 */

const fs = require('fs');
const path = require('path');

const CACHE_PATH = process.env.LID_CACHE_PATH || '/home/openclaw/.openclaw/workspace/LID_CACHE.json';

function parseArgs(args) {
  const result = { aliases: [] };
  for (let i = 0; i < args.length; i++) {
    switch (args[i]) {
      case '--lid': result.lid = args[++i]; break;
      case '--name': result.name = args[++i]; break;
      case '--phone': result.phone = args[++i]; break;
      case '--group': result.group = args[++i]; break;
      case '--alias': result.aliases.push(args[++i]); break;
      case '--help': case '-h': return null;
    }
  }
  return result;
}

function main() {
  const opts = parseArgs(process.argv.slice(2));

  if (!opts || !opts.lid || !opts.name) {
    console.log(`Usage: node add-member.js --lid <LID> --name <Name> [options]

Options:
  --lid <LID>        WhatsApp LID (required)
  --name <Name>      Display name (required)
  --phone <Phone>    Phone number (e.g., 1234567890)
  --group <GroupJID> Add to specific group (e.g., [REDACTED_SECRET])
  --alias <Alias>    Additional alias (can be repeated)

Example:
  node add-member.js --lid 123456789012345 --name "John" --phone 1234567890 --alias "john" --group "[REDACTED_SECRET]"
`);
    process.exit(1);
  }

  let cache;
  try {
    cache = JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8'));
  } catch {
    cache = { _names: {}, _aliases: {} };
  }

  // Add to _names
  if (!cache._names) cache._names = {};
  cache._names[opts.lid] = opts.name;
  console.log(`Added name: ${opts.lid} → ${opts.name}`);

  // Add to _aliases
  if (!cache._aliases) cache._aliases = {};
  const autoAliases = [
    opts.name.toLowerCase(),
    opts.name.toLowerCase().replace(/\s+/g, ''),
  ];
  const allAliases = [...new Set([...autoAliases, ...opts.aliases.map(a => a.toLowerCase())])];
  for (const alias of allAliases) {
    cache._aliases[alias] = opts.lid;
    console.log(`Added alias: ${alias} → ${opts.lid}`);
  }

  // Add to group
  if (opts.group) {
    if (!cache[opts.group]) cache[opts.group] = {};
    cache[opts.group][opts.lid] = opts.phone || '';
    console.log(`Added to group: ${opts.group}`);
  }

  // Add to ALL existing groups if phone is provided
  if (opts.phone && !opts.group) {
    let groupCount = 0;
    for (const key of Object.keys(cache)) {
      if (key.startsWith('_')) continue;
      if (!cache[key][opts.lid]) {
        cache[key][opts.lid] = opts.phone;
        groupCount++;
      }
    }
    if (groupCount) console.log(`Added to ${groupCount} existing groups`);
  }

  fs.writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2));
  console.log(`\nSaved to ${CACHE_PATH}`);
}

main();


path: install.sh
content: #!/bin/bash
# OpenClaw WhatsApp @Mention Skill - Installer
# Patches deliver-reply and login to enable model-agnostic @mentions
#
# WHAT THIS SCRIPT DOES:
#   1. Backs up deliver-reply-*.js and login-*.js (timestamped .bak files)
#   2. Patches deliver-reply to intercept outgoing messages and convert @mentions
#   3. Patches login's reply() to accept {text, mentions} objects
#   4. Creates LID_CACHE.json if not exists (empty cache, no credentials)
#   5. Copies mention-guide.md to AI memory directory
#   6. Restarts OpenClaw service
#
# This script does NOT access, read, or modify any credential/auth files.
# Run 'bash uninstall.sh' to restore original files from backup.
set -e

echo "╔══════════════════════════════════════════╗"
echo "║  OpenClaw @Mention Skill Installer v10   ║"
echo "╚══════════════════════════════════════════╝"
echo ""

# Find OpenClaw dist directory
DIST="/usr/lib/node_modules/openclaw/dist"
if [ ! -d "$DIST" ]; then
  echo "✗ OpenClaw not found at $DIST"
  echo "  Set OPENCLAW_DIST env to override."
  exit 1
fi

DELIVER=$(ls "$DIST"/deliver-reply-*.js 2>/dev/null | head -1)
LOGIN=$(ls "$DIST"/login-*.js 2>/dev/null | head -1)
WORKSPACE="/home/openclaw/.openclaw/workspace"
MEMORY="$WORKSPACE/memory"
LID_CACHE="$WORKSPACE/LID_CACHE.json"

if [ -z "$DELIVER" ] || [ -z "$LOGIN" ]; then
  echo "✗ Could not find deliver-reply or login files in $DIST"
  exit 1
fi

echo "Found:"
echo "  deliver-reply: $(basename $DELIVER)"
echo "  login: $(basename $LOGIN)"
echo ""

# Backup
TS=$(date -u +%Y%m%dT%H%M%S)
cp "$DELIVER" "${DELIVER}.bak.mention.${TS}"
cp "$LOGIN" "${LOGIN}.bak.mention.${TS}"
echo "✓ Backups created"

# ═══════════════════════════════════════════
# PATCH 1: Fix const chunk → let chunk
# ═══════════════════════════════════════════
python3 << 'PYEOF'
import sys

path = sys.argv[1] if len(sys.argv) > 1 else None
if not path:
    import glob
    files = glob.glob("/usr/lib/node_modules/openclaw/dist/deliver-reply-*.js")
    path = files[0] if files else None

with open(path) as f:
    c = f.read()

old = "for (const [index, chunk] of textChunks.entries()) {"
new = "for (const [index, _origChunk] of textChunks.entries()) {\n\t\t\tlet chunk = _origChunk;"

if "_origChunk" in c:
    print("  const→let: already fixed")
elif old in c:
    c = c.replace(old, new, 1)
    with open(path, "w") as f:
        f.write(c)
    print("✓ Fixed const chunk → let chunk")
else:
    print("⚠ const chunk pattern not found (may be a different OpenClaw version)")
PYEOF

# ═══════════════════════════════════════════
# PATCH 2: deliver-reply - V10 mention patch
# ═══════════════════════════════════════════
python3 << 'PYEOF'
import sys, glob

files = glob.glob("/usr/lib/node_modules/openclaw/dist/deliver-reply-*.js")
path = files[0]

with open(path) as f:
    c = f.read()

if "_LID_PATCH_V10" in c:
    print("  deliver-reply: V10 already applied")
    sys.exit(0)

# Remove any previous patch version
for marker in ["_LID_PATCH_V9", "_LID_PATCH_V8", "_LID_PATCH_V7", "_LID_PATCH_V6", "_LID_PATCH_V5"]:
    if marker in c:
        old_start = c.index("// " + marker)
        search_from = c.index("} else {", old_start)
        end_line = 'await sendWithRetry(() => msg.reply(chunk), "text");'
        end_pos = c.index(end_line, search_from)
        end = end_pos + len(end_line)
        while end < len(c) and c[end] in '\n\t\r ':
            end += 1
        if end < len(c) and c[end] == '}':
            end += 1
        c = c[:old_start] + 'await sendWithRetry(() => msg.reply(chunk), "text");' + c[end:]
        print(f"  Removed old {marker}")
        break

# Find the insertion point: before msg.reply(chunk) in text-only branch
target = '\t\t\tawait sendWithRetry(() => msg.reply(chunk), "text");'
if target not in c:
    # Try with different indentation
    target = 'await sendWithRetry(() => msg.reply(chunk), "text");'

if target not in c:
    print("✗ Could not find msg.reply(chunk) insertion point")
    sys.exit(1)

patch = """// _LID_PATCH_V10 - model-agnostic @mention (text=@LID, mentions=[LID@lid])
					let _v10m = [];
					try {
						const { readFileSync: _r } = await import("node:fs");
						const _c = JSON.parse(_r("/home/openclaw/.openclaw/workspace/LID_CACHE.json", "utf8"));
						const _g = msg.from || "";
						const _gm = _c[_g] || {};
						const _nm = _c._names || {};
						const _al = _c._aliases || {};
						const _gl = new Set(Object.keys(_gm));
						const _p2l = {};
						for (const [l, p] of Object.entries(_gm)) { if (p) _p2l[p] = l; }
						const _n2l = {};
						for (const [l, n] of Object.entries(_nm)) {
							if (n && (_gl.size === 0 || _gl.has(l))) _n2l[n.toLowerCase()] = l;
						}
						for (const [a, l] of Object.entries(_al)) {
							if ((_gl.size === 0 || _gl.has(l)) && !_n2l[a]) _n2l[a] = l;
						}
						const _ms = new Set();
						const _sn = Object.entries(_n2l).sort((a, b) => b[0].length - a[0].length);
						for (const [name, lid] of _sn) {
							const s = "@" + name;
							if (chunk.toLowerCase().includes(s)) {
								let r = "", p = 0, src = chunk;
								while (true) {
									const f = src.toLowerCase().indexOf(s, p);
									if (f === -1) { r += src.slice(p); break; }
									r += src.slice(p, f) + "@" + lid;
									p = f + s.length;
									_ms.add(lid + "@lid");
								}
								chunk = r;
							}
						}
						const _pr = /@(\\d{8,15})/g;
						let _pm;
						const _pf = [];
						while ((_pm = _pr.exec(chunk)) !== null) { _pf.push({ n: _pm[1], i: _pm.index }); }
						for (let i = _pf.length - 1; i >= 0; i--) {
							const { n: num, i: idx } = _pf[i];
							const lid = _p2l[num];
							if (lid && !_ms.has(lid + "@lid")) {
								chunk = chunk.slice(0, idx + 1) + lid + chunk.slice(idx + 1 + num.length);
								_ms.add(lid + "@lid");
							}
						}
						const _lr = /@(\\d{10,20})/g;
						let _lm;
						while ((_lm = _lr.exec(chunk)) !== null) { _ms.add(_lm[1] + "@lid"); }
						_v10m = [..._ms];
					} catch(_e) {}
					if (_v10m.length > 0) {
						await sendWithRetry(() => msg.reply({ text: chunk, mentions: _v10m }), "text");
					} else {
						await sendWithRetry(() => msg.reply(chunk), "text");
					}"""

# Replace the first occurrence only (text-only branch, not media fallback)
idx = c.index(target)
c = c[:idx] + patch + c[idx + len(target):]

with open(path, "w") as f:
    f.write(c)
print("✓ deliver-reply: V10 patch applied")
PYEOF

# ═══════════════════════════════════════════
# PATCH 3: login - reply() accepts {text, mentions}
# ═══════════════════════════════════════════
python3 << 'PYEOF'
import glob

files = glob.glob("/usr/lib/node_modules/openclaw/dist/login-*.js")
path = files[0]

with open(path) as f:
    c = f.read()

if "textOrPayload" in c:
    print("  login reply(): already patched")
else:
    # Try standard format
    old = "const reply = async (text) => {\n\t\t\tawait sendTrackedMessage(chatJid, { text });\n\t\t};"
    new = "const reply = async (textOrPayload) => {\n\t\t\tconst payload = typeof textOrPayload === 'string' ? { text: textOrPayload } : textOrPayload;\n\t\t\tawait sendTrackedMessage(chatJid, payload);\n\t\t};"

    if old in c:
        c = c.replace(old, new, 1)
        with open(path, "w") as f:
            f.write(c)
        print("✓ login: reply() patched to accept {text, mentions}")
    else:
        # Check if there's an older mention patch in reply()
        import re
        pattern = r"const reply = async \(text\) => \{[^}]+sendTrackedMessage[^}]+\};"
        match = re.search(pattern, c)
        if match:
            c = c.replace(match.group(0), new, 1)
            with open(path, "w") as f:
                f.write(c)
            print("✓ login: reply() replaced (had old inline mention logic)")
        else:
            print("⚠ login: reply() format not recognized - manual patch may be needed")
PYEOF

# ═══════════════════════════════════════════
# Initialize LID_CACHE if not exists
# ═══════════════════════════════════════════
if [ ! -f "$LID_CACHE" ]; then
  echo '{"_names":{},"_aliases":{}}' > "$LID_CACHE"
  chown openclaw:openclaw "$LID_CACHE"
  echo "✓ Created empty LID_CACHE.json"
else
  echo "  LID_CACHE.json already exists"
fi

# ═══════════════════════════════════════════
# Install mention-guide.md to AI memory
# ═══════════════════════════════════════════
mkdir -p "$MEMORY"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ -f "$SCRIPT_DIR/mention-guide.md" ]; then
  cp "$SCRIPT_DIR/mention-guide.md" "$MEMORY/mention-guide.md"
  chown openclaw:openclaw "$MEMORY/mention-guide.md"
  echo "✓ Installed mention-guide.md to AI memory"
else
  echo "  mention-guide.md not found in repo, skipping"
fi

# ═══════════════════════════════════════════
# Install add-member.js utility
# ═══════════════════════════════════════════
if [ -f "$SCRIPT_DIR/add-member.js" ]; then
  cp "$SCRIPT_DIR/add-member.js" "$WORKSPACE/scripts/add-member.js" 2>/dev/null || \
  cp "$SCRIPT_DIR/add-member.js" "$WORKSPACE/add-member.js"
  echo "✓ Installed add-member.js"
fi

# ═══════════════════════════════════════════
# Restart OpenClaw
# ═══════════════════════════════════════════
echo ""
if systemctl is-active openclaw >/dev/null 2>&1; then
  systemctl restart openclaw
  sleep 3
  if systemctl is-active openclaw >/dev/null 2>&1; then
    echo "✓ OpenClaw restarted successfully"
  else
    echo "✗ OpenClaw failed to start! Check: journalctl -u openclaw --no-pager -n 20"
    exit 1
  fi
else
  echo "⚠ OpenClaw service not running. Start it manually."
fi

echo ""
echo "══════════════════════════════════════════"
echo "  @Mention Skill installed successfully!"
echo "  Re-run this script after OpenClaw updates."
echo "══════════════════════════════════════════"


path: mention-guide.md
content: # WhatsApp @Mention Guide

## How to @mention group members

When you need to @mention someone in a WhatsApp group, use their **LID** (WhatsApp internal ID).

### Format
```
@LID_NUMBER
```

Example: `@123456789012345` will create a blue clickable mention.

### DO NOT use:
- `@Name` (won't be blue)
- `@PhoneNumber` (won't be blue)

### Finding LIDs

Check the LID cache file for known members:
```
/home/openclaw/.openclaw/workspace/LID_CACHE.json
```

The `_names` section maps LID → display name.
The `_aliases` section maps alias → LID.

### Rules
1. Always use `@LID` format for mentions
2. The system automatically converts `@Name` to `@LID`, but direct LID is most reliable
3. If you don't know someone's LID, check the LID cache file
4. New members are auto-discovered when they send messages in a group


path: uninstall.sh
content: #!/bin/bash
# Uninstall @Mention Skill - restore original files from backup
set -e

DIST="/usr/lib/node_modules/openclaw/dist"

echo "Restoring backups..."

for pattern in deliver-reply login; do
  FILE=$(ls "$DIST"/${pattern}-*.js 2>/dev/null | grep -v '.bak.' | head -1)
  BACKUP=$(ls -t "$DIST"/${pattern}-*.js.bak.mention.* 2>/dev/null | head -1)
  if [ -n "$BACKUP" ] && [ -n "$FILE" ]; then
    cp "$BACKUP" "$FILE"
    echo "✓ Restored $(basename $FILE) from $(basename $BACKUP)"
  else
    echo "⚠ No backup found for $pattern"
  fi
done

echo ""
echo "Restarting OpenClaw..."
systemctl restart openclaw 2>/dev/null && echo "✓ Restarted" || echo "⚠ Restart manually"
echo ""
echo "@Mention skill removed. LID_CACHE.json and mention-guide.md were NOT deleted."


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