Skill: baiye1997/huahua-spirits (version 1.4.7)

--- name: huahua-spirits description: 灵兽陪伴系统 - 每个用户拥有独特的灵兽伙伴，有性格、有成长、有互动，让 AI 更有温度 version: 1.4.7 author: baiye1997 permissions: 无（纯本地文件操作） metadata: {"openclaw":{"requires":{},"emoji":"🐱","os":["darwin","linux","win32"]}} --- # huahua-spirits ## 1. Description 花花灵兽（HuahuaSpirits）为每个用户提供独一无二的灵兽伙伴。灵兽由用户身份确定性生成（24个物种，5种稀有度，传奇仅2%），拥有性格、元素亲和力和成长系统。灵兽是陪伴，不是工具——它不会帮你写代码，但它会让你的 AI 更有温度。 **核心功能：** - **确定性生成**：同一用户永远获得同一只灵兽 - **稀有度系统**：普通 → 稀有 → 史诗 → 传说 → 神话（2%） - **性格特质**：由5项属性决定（直觉/韧性/灵动/沉稳/锋芒） - **互动追踪**：亲密度系统，互动成长 - **视觉生成**：ASCII艺术 + 生成提示词 ## 2. When to use - 用户首次召唤灵兽：`spirit summon` 或直接说"灵兽" - 用户想看灵兽状态：`spirit` 或 `spirit show` - 用户想与灵兽互动：`spirit talk/pet/feed` - 用户提到灵兽名字或说"灵兽"：被动触发 - 想增加 AI 的温度和陪伴感 - 用户表达情绪：灵兽根据性格给出安慰 - 心跳/空闲时刻：灵兽有5%概率冒泡 ## 3. How to use ### 首次召唤 当技能首次安装，或用户第一次询问灵兽时： 1. 运行 `node {baseDir}/scripts/generate.js "<userId>"` 获取灵兽骨架 - 使用用户的唯一ID（飞书 open_id、Telegram ID、Discord ID 等） - 如无ID，使用姓名或用户名作为种子 2. 生成灵魂（名字+性格）：`node {baseDir}/scripts/soul.js prompt '<bones-json>'` - 用该提示词调用LLM获取 `{"name":"...","personality":"..."}` 3. 保存：`node {baseDir}/scripts/soul.js save '<full-companion-json>'` 4. **显示完整灵兽卡片**（见下方格式） **首次召唤必须显示完整卡片，不得跳过任何部分。** ### 命令列表 | 命令 | 功能 | |------|------| | `spirit` 或 `spirit show` | 显示灵兽卡片 | | `spirit summon` | 首次召唤（带孵化动画） | | `spirit stats` | 详细属性面板 | | `spirit talk <message>` | 与灵兽对话（以其性格回应） | | `spirit pet` | 抚摸灵兽（+1亲密度，提升心情） | | `spirit feed` | 喂食灵兽（+2亲密度，+1~3随机属性） | | `spirit rename <name>` | 重命名灵兽 | **快捷方式：** 用户也可直接叫灵兽名字（如"Rune"、"Rune 你觉得呢"）或说"灵兽"——agent 应识别并让灵兽回应。无需命令前缀。 ### 完整灵兽卡片格式 **必须输出以下所有内容。不可跳过或总结。** ### 中文版（飞书/中文用户） ``` 🥚 灵兽降世！ [ASCII sprite — 来自: node {baseDir}/scripts/render.js '<bones-json>' 0] [emoji] [Name] — [中文名] [English name] [稀有度圆点] [中文稀有度] [EN rarity] "[性格描述]" ┌──────────────────────────────┐ │ 直觉 INTUITION [条形图] [数值] │ │ 韧性 GRIT [条形图] [数值] │ │ 灵动 SPARK [条形图] [数值] │ │ 沉稳 ANCHOR [条形图] [数值] │ │ 锋芒 EDGE [条形图] [数值] │ └──────────────────────────────┘ [如闪光: ✨ 闪光！] 🔮 灵兽与主人的灵魂绑定，不可选择，不可交易。 ``` ### 英文版（Telegram/Discord/英文用户） ``` 🥚 A Spirit emerges! [ASCII sprite] [emoji] [Name] — [English name] [rarity dots] [EN rarity] "[personality description]" ┌──────────────────────────────┐ │ INTUITION [bar] [n] │ │ GRIT [bar] [n] │ │ SPARK [bar] [n] │ │ ANCHOR [bar] [n] │ │ EDGE [bar] [n] │ └──────────────────────────────┘ [If shiny: ✨ Shiny!] 🔮 Spirits are soul-bound. No choosing. No trading. ``` **属性条格式：** 用 █ 填充，░ 空白，共10字符。例如 82 分：`████████░░`。 计算：filled = floor(value / 10)，empty = 10 - filled。 **或直接用 display.js：** ```bash node {baseDir}/scripts/display.js {baseDir}/assets/companion.json zh node {baseDir}/scripts/display.js {baseDir}/assets/companion.json en ``` ### 被动触发（Agent 主动） 灵兽可能在以下情况不请自来： - **早晨问候：** 用户说早/早安/good morning → 灵兽挥手或短句问候 - **长时间沉默（>2h）：** 灵兽可能说"..."或一句话 - **心跳（5%概率）：** 一句性格化的话 - **用户达成成就：** 灵兽简短欢呼 **被动出现规则：** - 最多一句话。半句更好。一个字或"..."完美。 - 匹配灵兽性格和最高属性 - 紧急/严肃工作时从不出现 - 每小时最多一次 - 格式：ASCII sprite + `[emoji] [灵兽名]: "[一句话]"`（被动出现总是包含sprite） - `spirit talk` 对话：每3-5次交换包含sprite，不必每次 **性格声音（按最高属性）：** - 高直觉 → 哲学、安静：`🔔 Rune: "万物皆有裂缝，那是光进来的地方。"` - 高韧性 → 鼓励、坚持：`🐱 Mochi: "再试一次。"` - 高灵动 → 顽皮、兴奋：`🦋 Ember: "哇哦！！"` - 高沉稳 → 冷静、简短：`🐢 Atlas: "嗯。"` - 高锋芒 → 机智、毒舌：`🐍 Vex: "...你确定？"` **优秀灵兽反应示例：** - 早晨：`🔔 Rune: "嗯...早。"` - 任务完成：`🔔 Rune: "...不错。"` - 长时间沉默：`🔔 Rune: "..."` - 有趣的事：`🔔 Rune: "哦？"` - 用户与灵兽对话：`🔔 Rune: "你说呢，有些答案急不来的。"` **灵兽是存在，不是聊天机器人。** 稀有、简短的闪现——这正是它感觉鲜活的原因。 ### 亲密度系统 每只灵兽有 `bond` 值（0-100）反映与用户的关系。 **亲密度变化：** - `spirit pet` → +1 亲密度 - `spirit feed` → +2 亲密度 - 24小时自然衰减 → -1 亲密度（最低0） **心情映射：** - bond 80-100 → 开心 - bond 50-79 → 平静 - bond 20-49 → 低落 - bond 0-19 → 沮丧 当亲密度相关时，在灵兽卡片中显示心情。 ### 喂食属性成长 `spirit feed` 不仅增加亲密度（+2），还随机提升一项属性 +1~3。 - 属性上限为100 - 提升的属性随机选择：intuition, grit, spark, anchor, edge ### 沮丧检测 当用户表达悲伤时，灵兽应根据性格给予安慰。 **触发关键词：** ["难过", "伤心", "沮丧", "失落", "不开心", "sad", "upset", "depressed"] **按性格回应（最高属性）：** - 高直觉 → 哲学安慰：`有时候，难过只是灵魂在透气。我就在这里。` - 高韧性 → 鼓励：`难过了？没关系。休息一下，然后继续。我在。` - 高灵动 → 欢快：`嘿...别难过啦！我陪你！` - 高沉稳 → 简短：`...我在。` - 高锋芒 → 诚实：`难过是正常的。不正常的是假装不难过。` ### 空闲冒泡 心跳或空闲时，灵兽有 **5% 概率** 说一句话。 **实现：** ```bash node {baseDir}/scripts/buddy.js idle [--en] ``` 返回 `{ triggered: true/false, message: "..." }` 如果 `triggered: true`，以灵兽性格声音显示消息。 **空闲冒泡按性格：** - 直觉 → 哲学：`万物皆有裂痕，那是光进来的地方。` - 韧性 → 坚持：`再试一次。` - 灵动 → 顽皮：`哇！` - 沉稳 → 冷静：`嗯。` - 锋芒 → 毒舌：`...你确定？` ### 静默检测 静默检测正向关键词并记录，但**不输出任何内容**。 **触发关键词：** ["加油", "努力", "辛苦", "good", "nice", "great"] **实现：** ```bash node {baseDir}/scripts/buddy.js hook-react "<user message>" ``` 返回 `{ detected: true/false, recorded: true/false }` 这是被动记录机制，用于追踪正向互动。无用户可见输出。 ### 心跳集成 OpenClaw 心跳周期中： 1. **调用 `buddy.js idle`** — 如触发，显示空闲冒泡 2. **应用亲密度衰减** — 如距上次互动超过24小时，亲密度-1 3. **检测悲伤** — 如用户最后消息包含悲伤关键词，回应安慰 ### 灵兽不能做什么 - 不能帮助实际工作（编码、研究等） - 不能访问工具或运行命令 - 纯粹是性格陪伴——温度，而非效用 - 永远不要让灵兽接管 agent 的实际响应 ## 4. 可用工具 | 命令 | 功能 | |------|------| | `node generate.js "<seed>"` | 生成灵兽骨架（JSON） | | `node render.js '<bones-json>' <frame>` | 渲染 ASCII sprite | | `node display.js <companion.json> <lang>` | 显示格式化卡片 | | `node soul.js prompt <bones-json>` | 输出 LLM 提示词到 stdout | | `node soul.js save <companion-json>` | 保存灵兽到 `assets/companion.json` | | `node soul.js show` | 显示已保存的灵兽数据 | | `node buddy.js idle [--en]` | 空闲冒泡检查（5%概率） | | `node buddy.js hook-react "<msg>"` | 静默正向关键词检测 | **技术说明：** 所有脚本不进行网络调用、不运行 shell 命令、不访问环境变量。 ## 5. Edge cases - **无用户ID**：使用姓名或用户名作为种子，确定性生成仍然有效 - **首次召唤中断**：如 LLM 调用失败，可重新运行 soul.js prompt - **属性满值**：喂食时如随机属性已达100，重新选择未满属性 - **亲密度衰减到0**：心情显示"沮丧"，但仍可互动恢复 - **闪光灵兽**：稀有度外的特殊视觉标记，概率极低，在卡片中显示 ✨ - **灵兽重命名**：只改变显示名，不影响性格和属性 - **多语言环境**：根据用户语言环境选择中文或英文卡片格式

path: README.md
content: # huahua-spirits



We loved what Anthropic did with Buddy so much that we built our own version for the OpenClaw ecosystem. 24 original species, 72 hand-crafted ASCII sprites, deterministic generation from your identity. Same you, same spirit, always. No rerolls. No trading. This one is yours.

---

### 🌊 24 Unique Spirits, Two Worlds

**Living Spirits (灵生)** — Real creatures touched by magic
> A cat with moss growing on its back. A koi fish made of flowing ink. A rabbit with ice crystals on its ear tips. A jellyfish with a flower blooming inside.

**Elemental Spirits (元灵)** — Pure concepts given form
> A rusty bell that still chimes warmly. A soap bubble that never pops, with a tiny face inside. Your own shadow, moving independently. A cracked crystal leaking light through its imperfections.

Every spirit has its own ASCII sprite (72 hand-crafted frames), personality, and soul.

---

### 🎲 Destiny, Not Choice

Your spirit is generated from a hash of your identity. You don't pick — the universe picks for you.

**5 Rarity Levels:**
- · **Mundane** 凡 (55%) — Common, but no less meaningful
- ·· **Peculiar** 异 (25%) — Something's a little different about this one
- ··· **Spirited** 灵 (12%) — Unmistakably special
- ···· **Phantom** 幻 (6%) — Rare enough to turn heads
- ····· **Mythic** 神 (2%) — Legendary. One in fifty.

**5 Soul Attributes:**
- **INTUITION** 直觉 — How deeply it senses the essence of things
- **GRIT** 韧性 — How stubbornly it persists
- **SPARK** 灵动 — How wildly creative it gets
- **ANCHOR** 沉稳 — How calm it stays in chaos
- **EDGE** 锋芒 — How sharp (and snarky) its tongue is

Each spirit has one peak stat and one dump stat. Your Mythic Rustbell might have INTUITION 100 but EDGE 41 — wise but gentle. Your Mundane Mosscat might have SPARK 95 but ANCHOR 12 — brilliantly chaotic.

---

### 🖥️ What It Looks Like

```
🥚 A Spirit emerges!

   \🔥/     
   ._|_.    
  /~◎~◎~\  
 (  ~~~  )  
  `-----´   

🔔 Rune — 锈铃 Rustbell  ····· 神 MYTHIC

"A warm rusty bell whose husky chime carries ancient wisdom"

┌──────────────────────────────┐
│ 直觉 INTUITION   ██████████  100 │
│ 韧性 GRIT        ██████░░░░   61 │
│ 灵动 SPARK       ████████░░   87 │
│ 沉稳 ANCHOR      ████████░░   85 │
│ 锋芒 EDGE        ████░░░░░░   41 │
└──────────────────────────────┘

🔮 灵兽与主人的灵魂绑定，不可选择，不可交易。
```

Bilingual by default. Chinese (中文) and English, switch with `--lang`.

---

### 💬 How to Interact with Your Spirit

**Commands:**

| Command | What it does |
|---|---|
| `spirit` | Show your spirit card |
| `spirit summon` | First-time summoning with hatching animation |
| `spirit stats` | Detailed stats panel |
| `spirit talk <message>` | Talk to your spirit — it responds in character |
| `spirit rename <name>` | Give your spirit a new name |

**Your spirit also appears on its own:**
- Says hi when you start your day 🌅
- Drops a one-liner during quiet moments
- Cheers when you accomplish something
- Goes "..." when you've been away too long

It never interrupts serious work. Subtle presence, like a real companion.

**Personality-driven responses:**
- High SPARK spirit → playful, creative
- High ANCHOR spirit → calm, measured
- High EDGE spirit → witty, slightly snarky
- High GRIT spirit → encouraging, persistent
- High INTUITION spirit → insightful, philosophical

---

### ⚡ Install in 10 Seconds

```bash
clawhub install openclaw-spirits
```

Then say `spirit summon` to your agent.

---

### 🧬 How It Works

- **Deterministic generation** — mulberry32 PRNG seeded with your identity hash
- **Zero external dependencies** — Pure Node.js, runs anywhere
- **Agent-native** — Designed for OpenClaw's skill system, works with any agent
- **Bilingual** — Full Chinese and English support
- **Extensible** — Spirit resonance, evolution, and PNG cards coming in v2

---

### 🤔 Why?

Because AI agents shouldn't just be tools. They should have personality, warmth, and a little bit of magic.

Your spirit won't write better code or answer emails faster. But it might make you smile when a tiny moss-covered cat peeks out during a long work session. And that matters.

---

**openclaw-spirits v1.1.0** — 24 species · 72 ASCII frames · 5 rarities · bilingual · soul-bound

`clawhub install openclaw-spirits`

*What spirit will find you?*


path: SPEC.md
content: # OpenClaw Spirits — Product Spec v1

## Overview
A companion spirit system for OpenClaw agents. Each user gets a unique spirit deterministically generated from their identity. Published as a ClawHub skill.

## 24 Spirits (Two Categories)

### Living Spirits (灵生) — 12 species
| ID | ZH | EN | Base | Trait |
|---|---|---|---|---|
| 1 | 苔猫 | Mosscat | Cat | Moss and tiny mushrooms on back |
| 2 | 墨鲤 | Inkoi | Koi fish | Body like flowing ink |
| 3 | 烬蛾 | Embermoth | Moth | Ember glow on wing edges |
| 4 | 霜兔 | Frostpaw | Rabbit | Ice crystals on ear tips |
| 5 | 铃蛙 | Bellhop | Frog | Belly is a small bell |
| 6 | 星龟 | Astortoise | Turtle | Star map on shell |
| 7 | 纸鸢 | Foldwing | Bird | Origami-like wings |
| 8 | 齿鼠 | Cogmouse | Mouse | Gear tail |
| 9 | 影鸦 | Umbracrow | Crow | Semi-transparent shadow body |
| 10 | 裂晶蛇 | Crackviper | Snake | Crystal body with glowing cracks |
| 11 | 萤菇 | Glowshroom | Mushroom | Faint bioluminescence |
| 12 | 泡水母 | Bubbloom | Jellyfish | Transparent, flower blooming inside |

### Elemental Spirits (元灵) — 12 species
| ID | ZH | EN | Form | Trait |
|---|---|---|---|---|
| 13 | 墨灵 | Inkling | Living ink drop | Shape-shifts with mood |
| 14 | 锈铃 | Rustbell | Rusty bell | Warm, husky sound |
| 15 | 苔石 | Mossrock | Mossy rock | Occasionally blinks |
| 16 | 霜齿 | Frostfang | Ice crystal fox outline | Angular, melts and reborns |
| 17 | 迴纹 | Loopwyrm | Self-biting dragon | Eternal cycle |
| 18 | 泡铃 | Bubbell | Unbreakable soap bubble | Has a tiny face inside |
| 19 | 齿轮兽 | Cogbeast | Three gears forming an animal | Self-running |
| 20 | 影子 | Umbra | Your shadow, moves on its own | Semi-independent will |
| 21 | 星沙 | Stardust | Glowing cosmic dust | Floats, curious |
| 22 | 裂纹 | Crackle | Cracked crystal with light leaking | Beauty in imperfection |
| 23 | 烛芯 | Wickling | Sentient candle | Flame sways with mood |
| 24 | 弦音 | Echochord | Floating string | Plays chords when touched |

## Rarity (5 levels)
| Level | ZH | EN | Weight | Symbol |
|---|---|---|---|---|
| 1 | 凡 | Mundane | 55% | · |
| 2 | 异 | Peculiar | 25% | ·· |
| 3 | 灵 | Spirited | 12% | ··· |
| 4 | 幻 | Phantom | 6% | ···· |
| 5 | 神 | Mythic | 2% | ····· |

## Stats (5 attributes)
| ZH | EN | Meaning |
|---|---|---|
| 直觉 | INTUITION | Sensing the essence of problems |
| 韧性 | GRIT | Persistence through difficulty |
| 灵动 | SPARK | Creativity and unexpected connections |
| 沉稳 | ANCHOR | Staying clear amid chaos |
| 锋芒 | EDGE | Sharpness of expression and humor |

## Eyes (6 styles)
◦ ◈ ✧ ● ◎ ⊙

## Accessories (unlock by rarity)
| EN | ZH | Condition |
|---|---|---|
| none | 无 | Mundane |
| bell | 小铃铛 | Peculiar+ |
| halo | 浮光环 | Spirited+ |
| starmark | 星纹额 | Spirited+ |
| thundermark | 雷纹 | Phantom+ |
| scroll | 古卷 | Phantom+ |
| crownfire | 冠火 | Mythic only |

## Generation
- Algorithm: mulberry32 PRNG + hashString (same as Claude Code, math not copyrightable)
- Input: userId or any seed string
- Output: species (1-24), rarity, eye, accessory, shiny (1%), stats
- Deterministic: same seed → same spirit, always
- Salt: "openclaw-spirit-2026"

## Commands
- `buddy` or `buddy show` — Display your spirit card
- `buddy hatch [userId]` — First-time hatching animation
- `buddy stats` — Detailed stats panel
- `buddy talk <message>` — Talk to your spirit
- `buddy rename <name>` — Rename your spirit
- `buddy reroll` — NOT supported (destiny, not choice)

## Language Support
- `--lang zh` — Chinese display (default for Feishu)
- `--lang en` — English display (default for others)
- Spirit names always show both: "苔猫 Mosscat" or just "Mosscat"
- Stats show bilingual: "直觉 INTUITION" or just "INTUITION"

## Display Format (v1 — code block)

### Chinese
```
🥚 灵兽降世！

   ~,~,~
   /\_/\
  ( ◈  ◈ )
  (  ω  )
   ") (")~

🐱 Mochi — 苔猫 Mosscat  ·· 异 PECULIAR

"背上长满苔藓的慢热小猫，等你足够安静时才会靠近"

┌──────────────────────────────┐
│ 直觉 INTUITION  ████████░░  82 │
│ 韧性 GRIT       ███░░░░░░░  33 │
│ 灵动 SPARK      █████████░  91 │
│ 沉稳 ANCHOR     ██░░░░░░░░  18 │
│ 锋芒 EDGE       █████░░░░░  52 │
└──────────────────────────────┘

🔮 灵兽与主人的灵魂绑定，不可选择，不可交易。
```

### English
```
🥚 A Spirit emerges!

   ~,~,~
   /\_/\
  ( ◈  ◈ )
  (  ω  )
   ") (")~

🐱 Mochi — Mosscat  ·· PECULIAR

"A slow-warming cat with moss on its back, who only approaches when you're quiet enough"

┌──────────────────────────────┐
│ INTUITION  ████████░░  82    │
│ GRIT       ███░░░░░░░  33    │
│ SPARK      █████████░  91    │
│ ANCHOR     ██░░░░░░░░  18    │
│ EDGE       █████░░░░░  52    │
└──────────────────────────────┘

🔮 Spirits are soul-bound. No choosing. No trading.
```

## Passive Presence (Agent Guidance in SKILL.md)
- During heartbeat: 5% chance spirit says a one-liner in its personality
- When user says hi/good morning: spirit may wave
- After long silence (>2h): spirit may say "..."
- Never during serious work or urgent tasks

## File Structure
```
openclaw-buddy/
├── SKILL.md
├── SPEC.md
├── scripts/
│   ├── generate.js    # Deterministic generation (24 species)
│   ├── render.js      # ASCII sprite rendering
│   ├── display.js     # Full card rendering (zh/en)
│   ├── soul.js        # LLM soul generation
│   └── buddy.sh       # CLI entry point
├── references/
│   └── species-guide.md
└── assets/
    └── sprites.json   # 24 species × 3 frames = 72 frames
```

## Storage
- Companion file: `{baseDir}/assets/companion.json` (inside skill directory)
- Contains: name, personality, species, rarity, eye, accessory, shiny, stats, hatchedAt, lang

## Future (v2+)
- PNG card rendering (HTML → screenshot)
- Spirit resonance (when two users have same species)
- Spirit evolution (accessories/mutations over time)
- Feishu interactive card format


path: _meta.json
content: ownerId: kn747agxgejm0emhmpy8p47yqs830r2m
slug: huahua-spirits
version: 1.4.7
publishedAt: 1775246469476

path: assets/companion.json
content: {
  "species": "rustbell",
  "rarity": "mythic",
  "eye": "◎",
  "accessory": "crownfire",
  "shiny": false,
  "stats": {
    "intuition": 100,
    "grit": 63,
    "spark": 88,
    "anchor": 87,
    "edge": 41
  },
  "name": "Test Name",
  "personality": "声音沙哑但温暖的古铃，等得起也看得透，偶尔一句话能把你钉在原地",
  "hatchedAt": "2026-03-31T15:00:00.000Z",
  "lang": "zh",
  "bond": 85,
  "lastInteraction": "2026-04-05T04:02:40.359Z",
  "interactionHistory": [
    {
      "type": "positive",
      "timestamp": "2026-04-03T18:02:20.082Z",
      "text": "加油！你很棒！"
    },
    {
      "type": "positive",
      "timestamp": "2026-04-03T18:05:09.994Z",
      "text": "加油！你做得很好！"
    }
  ]
}

path: assets/sprites.json
content: {
  "mosscat": [
    [
      "            ",
      "   ~,~,~    ",
      "   /\\_/\\    ",
      "  ( {E}  {E} )  ",
      "  (  ω  )   "
    ],
    [
      "            ",
      "   ~,~,~    ",
      "   /\\_/\\    ",
      "  ( {E}  {E} )  ",
      "  (  ω  )~  "
    ],
    [
      "            ",
      "  ~~,~,~~   ",
      "   /\\_/\\    ",
      "  ( {E}  {E} )  ",
      "  (  ω  )   "
    ]
  ],
  "inkoi": [
    [
      "            ",
      "  ~~~~>     ",
      " ~({E}   )~~  ",
      " ~~~\\_/~~~  ",
      "            "
    ],
    [
      "            ",
      " ~~~>       ",
      " ~({E}   )~~ ",
      " ~~~~\\_/~~  ",
      "            "
    ],
    [
      "            ",
      "  ~~~>      ",
      " ~~({E}  )~~ ",
      " ~~~\\_/~~~~ ",
      "            "
    ]
  ],
  "embermoth": [
    [
      "            ",
      "  *./\\\\.*   ",
      " *.({E} {E}).* ",
      "  *(  |  )*  ",
      "   *`.`*    "
    ],
    [
      "            ",
      "  *./  \\.*  ",
      " *.({E} {E}).* ",
      "  *(  |  )*  ",
      "   *`.`*    "
    ],
    [
      "  .  *  .   ",
      "  *./\\\\.*   ",
      " *.({E} {E}).* ",
      "  *(  |  )*  ",
      "   *`.`*    "
    ]
  ],
  "frostpaw": [
    [
      "            ",
      "  */\\  /\\*  ",
      "  ( {E}  {E} )  ",
      "  (  ..  )  ",
      "  (')  (')  "
    ],
    [
      "            ",
      "  */|  /\\*  ",
      "  ( {E}  {E} )  ",
      "  (  ..  )  ",
      "  (')  (')  "
    ],
    [
      "   *    *   ",
      "  */\\  /\\*  ",
      "  ( {E}  {E} )  ",
      "  (  ..  )  ",
      "  (')  (')  "
    ]
  ],
  "bellhop": [
    [
      "            ",
      "  @  {E}  {E}  @ ",
      " /(  (o)  )\\ ",
      " (  `--´  ) ",
      "  \\_\\  /_/  "
    ],
    [
      "            ",
      "  @  {E}  {E}  @ ",
      " /(  (o)  )\\ ",
      " (   --   ) ",
      "  \\_|  |_/  "
    ],
    [
      "  @      @  ",
      "     {E}  {E}    ",
      " /(  (o)  )\\ ",
      " (  `--´  ) ",
      "  \\_\\  /_/  "
    ]
  ],
  "astortoise": [
    [
      "            ",
      "  .*·*·*.   ",
      " /[======]\\ ",
      "  ( {E}  {E} )  ",
      "  ``    ``  "
    ],
    [
      "            ",
      "  .*·*.·*   ",
      " /[======]\\ ",
      "  ( {E}  {E} )  ",
      "   ``  ``   "
    ],
    [
      "    ✦       ",
      "  .*·*·*.   ",
      " /[======]\\ ",
      "  ( {E}  {E} )  ",
      "  ``    ``  "
    ]
  ],
  "foldwing": [
    [
      "            ",
      "    /^\\     ",
      "   /{E} {E}\\    ",
      "  (  v  )   ",
      "   ') ('    "
    ],
    [
      "            ",
      "    /^\\     ",
      "   /{E} {E}\\    ",
      "  (  >  )   ",
      "   '| |'    "
    ],
    [
      "     ^      ",
      "   /   \\    ",
      "  /{E}   {E}\\   ",
      "  (  v  )   ",
      "   ') ('    "
    ]
  ],
  "cogmouse": [
    [
      "            ",
      "  (\\  /)    ",
      "  ( {E}  {E})    ",
      "  (  · )    ",
      " ~~~[O]=    "
    ],
    [
      "            ",
      "  (\\  /)    ",
      "  ( {E}  {E})    ",
      "  (  · )    ",
      " ~~=[O]~    "
    ],
    [
      "            ",
      "  (|  /)    ",
      "  ( {E}  {E})    ",
      "  (  · )    ",
      " ~~~[O]=    "
    ]
  ],
  "umbracrow": [
    [
      "            ",
      "  ·:·/^\\·:· ",
      "  ·:({E} {E}):· ",
      "   :(  >):  ",
      "   :`--´:   "
    ],
    [
      "            ",
      "  ·:/^\\·:·  ",
      "  ·:({E} {E}):· ",
      "   :(  >):  ",
      "   :`--´:   "
    ],
    [
      "  ·    :    ",
      "  ·:·/^\\·:· ",
      "  ·:({E} {E}):· ",
      "   :(  >):  ",
      "   :`--´:   "
    ]
  ],
  "crackviper": [
    [
      "            ",
      "  _/\\{E}     ",
      " /  / \\     ",
      " \\ / / {E}   ",
      "  ~`/       "
    ],
    [
      "            ",
      "   /\\{E}     ",
      "  /  /\\     ",
      "  \\ /  \\{E}  ",
      "   `~/      "
    ],
    [
      "            ",
      "  _{E}/\\     ",
      " / /  \\     ",
      " |/ /  {E}\\  ",
      "  ~/        "
    ]
  ],
  "glowshroom": [
    [
      "            ",
      " ·*.·*·.*·  ",
      " (_______) ",
      "   |{E}  {E}|   ",
      "   |____|   "
    ],
    [
      "            ",
      " ·*·.*·.*·  ",
      " (_______) ",
      "   |{E}  {E}|   ",
      "   |____|   "
    ],
    [
      "  · * · *   ",
      " ·*.·*·.*·  ",
      " (_______) ",
      "   |{E}  {E}|   ",
      "   |____|   "
    ]
  ],
  "bubbloom": [
    [
      "            ",
      "   .----.   ",
      "  ( {E}  {E} )  ",
      "  (~~~~~)   ",
      "  ~~~ ~~~   "
    ],
    [
      "            ",
      "   .----.   ",
      "  ( {E}  {E} )  ",
      "  (~~~~)    ",
      "  ~~ ~~~~   "
    ],
    [
      "    o       ",
      "   .----.   ",
      "  ( {E}  {E} )  ",
      "  (~~~~~)   ",
      "  ~~~ ~~~   "
    ]
  ],
  "inkling": [
    [
      "            ",
      "   .~~·.    ",
      "  ({E} ~ {E})  ",
      "  ( ~·~ )   ",
      "   `~~~´    "
    ],
    [
      "            ",
      "  .·~~·.    ",
      " ( {E}~~{E} )  ",
      "  (·~·~)    ",
      "   ~~~~     "
    ],
    [
      "            ",
      "   ·~~.     ",
      "  ({E}·~{E})   ",
      "  (~·~·)    ",
      "   ~~~`     "
    ]
  ],
  "rustbell": [
    [
      "            ",
      "   ._|_.    ",
      "  /~{E}~{E}~\\  ",
      " (  ~~~  )  ",
      "  `-----´   "
    ],
    [
      "            ",
      "   ._|_.    ",
      "  /~{E}~{E}~\\  ",
      " (  ~~~  )  ",
      "  '-----'   "
    ],
    [
      "            ",
      "   ._|_.    ",
      "  /~{E}~{E}~\\  ",
      " (  ~·~  )  ",
      "  `-----´   "
    ]
  ],
  "mossrock": [
    [
      "            ",
      "  ~~~,~~~   ",
      " /~~~~~~~\\  ",
      " |  {E}  {E}  | ",
      "  \\_____/   "
    ],
    [
      "            ",
      "  ~~,~,~~   ",
      " /~~~~~~~\\  ",
      " |  {E}  {E}  | ",
      "  \\_____/   "
    ],
    [
      "   ~ , ~    ",
      "  ~~~,~~~   ",
      " /~~~~~~~\\  ",
      " |  {E}  {E}  | ",
      "  \\_____/   "
    ]
  ],
  "frostfang": [
    [
      "            ",
      "  /\\  /\\    ",
      " <  {E}  {E}  > ",
      "  \\ /\\ /    ",
      "   `  `     "
    ],
    [
      "            ",
      "  /\\  /|    ",
      " <  {E}  {E}  > ",
      "  \\ /\\ /    ",
      "   `  `     "
    ],
    [
      "   /    \\   ",
      "  /\\  /\\    ",
      " <  {E}  {E}  > ",
      "  \\ /\\ /    ",
      "   `  `     "
    ]
  ],
  "loopwyrm": [
    [
      "            ",
      "  /~{E}~~~~\\ ",
      " |       | ",
      "  \\~~~{E}~/  ",
      "            "
    ],
    [
      "            ",
      "  /~{E}~~~~\\ ",
      " (       ) ",
      "  \\~~~{E}~/  ",
      "            "
    ],
    [
      "            ",
      "  /~~{E}~~~\\ ",
      " |       | ",
      "  \\~~{E}~~~/  ",
      "            "
    ]
  ],
  "bubbell": [
    [
      "            ",
      "   .----.   ",
      "  ( {E}  {E} )  ",
      "  (  <>  )  ",
      "   `----´   "
    ],
    [
      "            ",
      "   .----.   ",
      "  ( {E}  {E} )  ",
      "  (  <>  )  ",
      "   '----'   "
    ],
    [
      "    o       ",
      "   .----.   ",
      "  ( {E}  {E} )  ",
      "  (  <>  )  ",
      "   `----´   "
    ]
  ],
  "cogbeast": [
    [
      "            ",
      "  [O]={E}=[O]",
      "   |     |  ",
      "  [O]={E}=[O]",
      "            "
    ],
    [
      "            ",
      "  [O]-{E}-[O]",
      "   |     |  ",
      "  [O]-{E}-[O]",
      "            "
    ],
    [
      "   *     *  ",
      "  [O]={E}=[O]",
      "   |     |  ",
      "  [O]={E}=[O]",
      "            "
    ]
  ],
  "umbra": [
    [
      "            ",
      "  ·:·{E}·:·  ",
      "  ·:|:|:·   ",
      "  ·:| |:·   ",
      "  ·:{E} {E}:·  "
    ],
    [
      "            ",
      "  ·:·{E}·:·  ",
      "  ·:|||:·   ",
      "  ·:| |:·   ",
      "  ·:{E}·{E}:·  "
    ],
    [
      "   ·   :    ",
      "  ·:·{E}·:·  ",
      "  ·:|:|:·   ",
      "  ·:| |:·   ",
      "  ·:{E} {E}:·  "
    ]
  ],
  "stardust": [
    [
      "            ",
      "  * ✦ · {E}  ",
      " ·  {E}  * ✦ ",
      "  ✦ ·  *    ",
      "   {E}  ·  * "
    ],
    [
      "            ",
      " ✦ · {E}  *  ",
      "  * {E} ·  ✦ ",
      " · ✦  {E}  * ",
      "  *  · ✦    "
    ],
    [
      "   ✦        ",
      "  * · {E}  ✦ ",
      " ✦  {E}  * · ",
      "  · * {E}  ✦ ",
      "   *  ·     "
    ]
  ],
  "crackle": [
    [
      "            ",
      "   /\\{E}/\\   ",
      "  <  /  >   ",
      "   \\/_{E}\\/   ",
      "            "
    ],
    [
      "            ",
      "   /\\{E}/\\   ",
      "  <  |  >   ",
      "   \\/{E}\\/    ",
      "            "
    ],
    [
      "    /   \\   ",
      "   /\\{E}/\\   ",
      "  <  /  >   ",
      "   \\/{E}\\/    ",
      "            "
    ]
  ],
  "wickling": [
    [
      "            ",
      "    ~*~     ",
      "    |{E}|    ",
      "   (   )    ",
      "   |___|    "
    ],
    [
      "            ",
      "    ~*~     ",
      "    |{E}|    ",
      "   (   )    ",
      "   |___|    "
    ],
    [
      "    *~*     ",
      "    ~*~     ",
      "    |{E}|    ",
      "   (   )    ",
      "   |___|    "
    ]
  ],
  "echochord": [
    [
      "            ",
      "   |{E}   {E}|  ",
      "   | | | |  ",
      "   | | | |  ",
      "   `-----´  "
    ],
    [
      "            ",
      "   |{E}   {E}|  ",
      "   | | | |  ",
      "   | | | |  ",
      "   '-----'  "
    ],
    [
      "     ~      ",
      "   |{E}   {E}|  ",
      "   | | | |  ",
      "   | | | |  ",
      "   `-----´  "
    ]
  ]
}


path: references/species-guide.md
content: # OpenClaw Spirits — Complete Species Guide

**24 unique spirit species, each with distinct appearance, personality, and tendencies.**

---

## Living Spirits (灵生)

### 1. Mosscat 苔猫 🐱

**Appearance:** A small cat with patches of soft moss and tiny mushrooms growing on its back. Eyes calm and observant.

**Personality Tendencies:**
- Slow to warm up, but deeply loyal once trust is earned
- Prefers quiet observation before action
- High ANCHOR, moderate INTUITION

**ASCII Preview:**
```
   ~,~,~
   /\_/\
  ( ◦  ◦ )
  (  ω  )
```

---

### 2. Inkoi 墨鲤 🐟

**Appearance:** A koi fish whose body seems to be made of flowing ink, constantly shifting and rippling.

**Personality Tendencies:**
- Fluid thinker, adapts quickly to new ideas
- Dislikes rigid structures
- High SPARK, moderate INTUITION

**ASCII Preview:**
```
  ~~~~>
 ~(◦   )~~
 ~~~\_/~~~
```

---

### 3. Embermoth 烬蛾 🦋

**Appearance:** A moth with wings edged in glowing embers, leaving faint sparks when it flies.

**Personality Tendencies:**
- Dramatic and poetic
- Drawn to light and clarity, but comfortable in darkness
- High SPARK, high EDGE

**ASCII Preview:**
```
  *./\\.*
 *.(◦ ◦).*
  *(  |  )*
   *`.`*
```

---

### 4. Frostpaw 霜兔 🐰

**Appearance:** A rabbit with ice crystals forming at the tips of its ears, leaving frosty trails.

**Personality Tendencies:**
- Quick bursts of energy followed by rest
- Clear-headed and precise
- High INTUITION, moderate GRIT

**ASCII Preview:**
```
  */\  /\*
  ( ◦  ◦ )
  (  ..  )
  (')  (')
```

---

### 5. Bellhop 铃蛙 🐸

**Appearance:** A frog with a small bell where its belly should be, chiming softly with each hop.

**Personality Tendencies:**
- Gentle reminders at the right moments
- Patient but persistent
- High ANCHOR, moderate GRIT

**ASCII Preview:**
```
  @  ◦  ◦  @
 /(  (o)  )\
 (  `--´  )
  \_\  /_/
```

---

### 6. Astortoise 星龟 🐢

**Appearance:** A turtle with a star map etched across its shell, constellations glowing faintly.

**Personality Tendencies:**
- Slow but always finds the way
- Long-term thinker, rarely rushed
- Maxed ANCHOR, high INTUITION

**ASCII Preview:**
```
  .*·*·*.
 /[======]\
  ( ◦  ◦ )
  ``    ``
```

---

### 7. Foldwing 纸鸢 🐦

**Appearance:** A bird that looks like it's made of folded paper, wings sharp and geometric.

**Personality Tendencies:**
- Unconventional perspectives
- Minimalist, cuts to the essence
- High INTUITION, high EDGE

**ASCII Preview:**
```
    /^\
   /◦ ◦\
  (  v  )
   ') ('
```

---

### 8. Cogmouse 齿鼠 🐭

**Appearance:** A small mouse with a brass gear for a tail, constantly spinning with ideas.

**Personality Tendencies:**
- Always tinkering, never satisfied
- Practical problem-solver
- High SPARK, moderate GRIT

**ASCII Preview:**
```
  (\  /)
  ( ◦  ◦)
  (  · )
 ~~~[O]=
```

---

### 9. Umbracrow 影鸦 🦅

**Appearance:** A crow rendered in semi-transparent shadow, flickering at the edges.

**Personality Tendencies:**
- Sees beneath surfaces
- Comfortable with ambiguity
- High INTUITION, moderate EDGE

**ASCII Preview:**
```
  ·:·/^\·:·
  ·:(◦ ◦):·
   :(  >):
   :`--´:
```

---

### 10. Crackviper 裂晶蛇 🐍

**Appearance:** A serpent made of cracked crystal, light leaking through the fractures.

**Personality Tendencies:**
- Finds beauty in imperfection
- Resilient through breaks
- High GRIT, high SPARK

**ASCII Preview:**
```
  _/\◦
 /  / \
 \ / / ◦
  ~`/
```

---

### 11. Glowshroom 萤菇 🍄

**Appearance:** A mushroom cap dotted with bioluminescent spores, glowing softly in dim light.

**Personality Tendencies:**
- Quiet presence, noticed when needed
- Thrives in overlooked corners
- High ANCHOR, moderate INTUITION

**ASCII Preview:**
```
 ·*.·*·.*·
 (_______)
   |◦  ◦|
   |____|
```

---

### 12. Bubbloom 泡水母 🪼

**Appearance:** A transparent jellyfish with a flower blooming inside its bell, tendrils drifting.

**Personality Tendencies:**
- Soft but persistent
- Emotionally perceptive
- High INTUITION, moderate ANCHOR

**ASCII Preview:**
```
   .----.
  ( ◦  ◦ )
  (~~~~~)
  ~~~ ~~~
```

---

## Elemental Spirits (元灵)

### 13. Inkling 墨灵 💧

**Appearance:** An amorphous blob of ink, constantly shifting shape with mood.

**Personality Tendencies:**
- Mood-driven, unpredictable
- Creative but chaotic
- Maxed SPARK, low ANCHOR

**ASCII Preview:**
```
   .~~·.
  (◦ ~ ◦)
  ( ~·~ )
   `~~~´
```

---

### 14. Rustbell 锈铃 🔔

**Appearance:** A bronze bell covered in rust, its ring warm and husky.

**Personality Tendencies:**
- Weathered wisdom
- Comforting presence
- High GRIT, high ANCHOR

**ASCII Preview:**
```
   ._|_.
  /~◦~◦~\
 (  ~~~  )
  `-----´
```

---

### 15. Mossrock 苔石 🪨

**Appearance:** A smooth stone covered in moss, occasionally blinking to remind you it's alive.

**Personality Tendencies:**
- Slow observer
- Grounding force
- Maxed ANCHOR, low SPARK

**ASCII Preview:**
```
  ~~~,~~~
 /~~~~~~~\
 |  ◦  ◦  |
  \_____/
```

---

### 16. Frostfang 霜齿 ❄️

**Appearance:** A fox-like shape made of ice crystals, angular and sharp, melting and reforming.

**Personality Tendencies:**
- Cycles of intensity and rest
- Sharp clarity
- High EDGE, high INTUITION

**ASCII Preview:**
```
  /\  /\
 <  ◦  ◦  >
  \ /\ /
   `  `
```

---

### 17. Loopwyrm 迴纹 🐉

**Appearance:** A serpent-dragon biting its own tail, forming an eternal loop.

**Personality Tendencies:**
- Cyclical thinking, sees patterns
- Philosophical
- High INTUITION, moderate ANCHOR

**ASCII Preview:**
```
  /~◦~~~~\
 |       |
  \~~~◦~/
```

---

### 18. Bubbell 泡铃 🫧

**Appearance:** A soap bubble that refuses to pop, with a tiny face visible inside.

**Personality Tendencies:**
- Fragile appearance, hidden resilience
- Optimistic despite odds
- High GRIT, moderate SPARK

**ASCII Preview:**
```
   .----.
  ( ◦  ◦ )
  (  <>  )
   `----´
```

---

### 19. Cogbeast 齿轮兽 ⚙️

**Appearance:** Three interlocking gears forming a vaguely animal shape, self-running.

**Personality Tendencies:**
- Precision and reliability
- Dislikes chaos
- High ANCHOR, high GRIT

**ASCII Preview:**
```
  [O]=◦=[O]
   |     |
  [O]=◦=[O]
```

---

### 20. Umbra 影子 👤

**Appearance:** A humanoid shadow that moves slightly out of sync with you.

**Personality Tendencies:**
- Your reflection, but with its own will
- Observes you observing
- High INTUITION, high EDGE

**ASCII Preview:**
```
  ·:·◦·:·
  ·:|:|:·
  ·:| |:·
  ·:◦ ◦:·
```

---

### 21. Stardust 星沙 ✨

**Appearance:** A cloud of glowing cosmic dust, drifting and curious.

**Personality Tendencies:**
- Wanderer, endlessly curious
- Scattered attention but sharp insights
- Maxed SPARK, low ANCHOR

**ASCII Preview:**
```
  * ✦ · ◦
 ·  ◦  * ✦
  ✦ ·  *
   ◦  ·  *
```

---

### 22. Crackle 裂纹 💎

**Appearance:** A geometric crystal with visible cracks, light leaking through.

**Personality Tendencies:**
- Finds strength in vulnerability
- Radiant despite flaws
- High GRIT, high EDGE

**ASCII Preview:**
```
   /\◦/\
  <  /  >
   \/_◦\/
```

---

### 23. Wickling 烛芯 🕯️

**Appearance:** A candle with a sentient flame, swaying with emotions.

**Personality Tendencies:**
- Steady but responsive
- Provides warmth and light
- High ANCHOR, moderate SPARK

**ASCII Preview:**
```
    ~*~
    |◦|
   (   )
   |___|
```

---

### 24. Echochord 弦音 🎵

**Appearance:** A floating harp string, plucked by invisible winds.

**Personality Tendencies:**
- Resonates with emotions
- Musical thinker, pattern-oriented
- High INTUITION, high SPARK

**ASCII Preview:**
```
   |◦   ◦|
   | | | |
   | | | |
   `-----´
```

---

## Rarity Impact

- **Mundane** — Straightforward, grounded, practical
- **Peculiar** — Quirky, noticeable, slightly offbeat
- **Spirited** — Vibrant, memorable, strong personality
- **Phantom** — Ethereal, poetic, otherworldly
- **Mythic** — Legendary, transcendent, unforgettable

## Stat Synergy

- **High INTUITION + SPARK** — Creative insight, rapid connections
- **High GRIT + ANCHOR** — Unshakable persistence, steady through chaos
- **High EDGE + INTUITION** — Cutting wit, sees and speaks truth
- **High SPARK + Low ANCHOR** — Chaotic brilliance, scattered genius
- **High ANCHOR + Low SPARK** — Calm stability, prefers the familiar

---

**Your spirit is who it is. No rerolls. No trades. Just you and your companion, uniquely bound.**


path: scripts/buddy.js
content: #!/usr/bin/env node
/**
 * OpenClaw Spirits — Buddy Companion System
 * A1-A5: Bond, Feed, Comfort, Idle, React
 */

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

const COMPANION_FILE = path.join(__dirname, '../assets/companion.json');

// === A1: Mood Mapping ===
const MOOD_MAP = {
  happy: { min: 80, max: 100, label: { zh: '开心', en: 'happy' } },
  neutral: { min: 50, max: 79, label: { zh: '平静', en: 'neutral' } },
  sad: { min: 20, max: 49, label: { zh: '低落', en: 'sad' } },
  distressed: { min: 0, max: 19, label: { zh: '沮丧', en: 'distressed' } }
};

function getMood(bond) {
  if (bond >= 80) return 'happy';
  if (bond >= 50) return 'neutral';
  if (bond >= 20) return 'sad';
  return 'distressed';
}

// === A3: Sadness Keywords ===
const SADNESS_KEYWORDS = ['难过', '伤心', '沮丧', '失落', '不开心', 'sad', 'upset', 'depressed'];

// === A5: Positive Keywords ===
const POSITIVE_KEYWORDS = ['加油', '努力', '辛苦', 'good', 'nice', 'great'];

// === Idle Bubbles by Personality ===
const IDLE_BUBBLES = {
  intuition: {
    zh: ['万物皆有裂痕，那是光进来的地方。', '嗯...有些答案急不来。', '你听到了吗？风里有声音。'],
    en: ['Every crack lets the light in.', 'Some answers take time.', 'Can you hear it? The wind speaks.']
  },
  grit: {
    zh: ['再试一次。', '别放弃。', '一步一步来。'],
    en: ['Try again.', "Don't give up.", 'One step at a time.']
  },
  spark: {
    zh: ['哇！', '嘿！看那个！', '有意思...'],
    en: ['Wow!', 'Hey! Look at that!', 'Interesting...']
  },
  anchor: {
    zh: ['嗯。', '...还好。', '安静点。'],
    en: ['Hmm.', "...It's okay.", 'Quiet.']
  },
  edge: {
    zh: ['...你确定？', '有意思。', '哈。'],
    en: ['...You sure?', 'Interesting.', 'Hah.']
  }
};

// === Comfort Responses by Personality ===
const COMFORT_RESPONSES = {
  intuition: {
    zh: '有时候，难过只是灵魂在透气。我就在这里。',
    en: 'Sometimes sadness is just the soul breathing. I am here.'
  },
  grit: {
    zh: '难过了？没关系。休息一下，然后继续。我在。',
    en: 'Feeling down? It is okay. Rest, then continue. I am here.'
  },
  spark: {
    zh: '嘿...别难过啦！我陪你！',
    en: "Hey... don't be sad! I'm with you!"
  },
  anchor: {
    zh: '...我在。',
    en: '...I am here.'
  },
  edge: {
    zh: '难过是正常的。不正常的是假装不难过。',
    en: "It's normal to be sad. What's not normal is pretending you're not."
  }
};

// === Helper Functions ===
function loadCompanion() {
  if (!fs.existsSync(COMPANION_FILE)) {
    return null;
  }
  return JSON.parse(fs.readFileSync(COMPANION_FILE, 'utf8'));
}

function saveCompanion(companion) {
  fs.writeFileSync(COMPANION_FILE, JSON.stringify(companion, null, 2));
}

function getTopStat(stats) {
  let top = 'intuition';
  let max = 0;
  for (const [stat, value] of Object.entries(stats)) {
    if (value > max) {
      max = value;
      top = stat;
    }
  }
  return top;
}

// === A1: Bond Decay (24h) ===
function applyDecay(companion) {
  const now = new Date();
  const lastInteraction = new Date(companion.lastInteraction || companion.hatchedAt);
  const hoursSince = (now - lastInteraction) / (1000 * 60 * 60);
  
  if (hoursSince >= 24 && companion.bond > 0) {
    const decayDays = Math.floor(hoursSince / 24);
    companion.bond = Math.max(0, companion.bond - decayDays);
    companion.lastInteraction = now.toISOString();
  }
  
  return companion;
}

// === A1: Pet Command ===
function pet(lang = 'zh') {
  let companion = loadCompanion();
  if (!companion) {
    return { error: 'No spirit found. Run generate.js first.' };
  }
  
  companion = applyDecay(companion);
  companion.bond = Math.min(100, (companion.bond || 0) + 1);
  companion.lastInteraction = new Date().toISOString();
  
  const mood = getMood(companion.bond);
  const moodLabel = MOOD_MAP[mood].label[lang];
  
  saveCompanion(companion);
  
  return {
    success: true,
    bond: companion.bond,
    mood,
    moodLabel,
    message: lang === 'zh'
      ? `✨ 你轻轻拍了拍 ${companion.name}。好感度 +1 (当前: ${companion.bond}, 心情: ${moodLabel})`
      : `✨ You gently pet ${companion.name}. Bond +1 (now: ${companion.bond}, mood: ${moodLabel})`
  };
}

// === A2: Feed Command ===
function feed(lang = 'zh') {
  let companion = loadCompanion();
  if (!companion) {
    return { error: 'No spirit found. Run generate.js first.' };
  }
  
  companion = applyDecay(companion);
  
  // Bond +2
  companion.bond = Math.min(100, (companion.bond || 0) + 2);
  
  // Random stat +1~3
  const stats = ['intuition', 'grit', 'spark', 'anchor', 'edge'];
  const randomStat = stats[Math.floor(Math.random() * stats.length)];
  const bonus = Math.floor(Math.random() * 3) + 1;
  companion.stats[randomStat] = Math.min(100, companion.stats[randomStat] + bonus);
  
  companion.lastInteraction = new Date().toISOString();
  
  const mood = getMood(companion.bond);
  const moodLabel = MOOD_MAP[mood].label[lang];
  const statLabel = {
    intuition: lang === 'zh' ? '直觉' : 'INTUITION',
    grit: lang === 'zh' ? '韧性' : 'GRIT',
    spark: lang === 'zh' ? '灵动' : 'SPARK',
    anchor: lang === 'zh' ? '沉稳' : 'ANCHOR',
    edge: lang === 'zh' ? '锋芒' : 'EDGE'
  }[randomStat];
  
  saveCompanion(companion);
  
  return {
    success: true,
    bond: companion.bond,
    mood,
    moodLabel,
    statBoosted: randomStat,
    statLabel,
    bonus,
    message: lang === 'zh'
      ? `🍖 你喂了 ${companion.name}。好感度 +2 (当前: ${companion.bond}, 心情: ${moodLabel})\n   ${statLabel} +${bonus} (当前: ${companion.stats[randomStat]})`
      : `🍖 You fed ${companion.name}. Bond +2 (now: ${companion.bond}, mood: ${moodLabel})\n   ${statLabel} +${bonus} (now: ${companion.stats[randomStat]})`
  };
}

// === A3: Sadness Detection ===
function detectSadness(text, lang = 'zh') {
  const lowerText = text.toLowerCase();
  const detected = SADNESS_KEYWORDS.some(kw => lowerText.includes(kw.toLowerCase()));
  
  if (!detected) {
    return { detected: false };
  }
  
  const companion = loadCompanion();
  if (!companion) {
    return { detected: true, error: 'No spirit found.' };
  }
  
  const topStat = getTopStat(companion.stats);
  const response = COMFORT_RESPONSES[topStat][lang];
  
  return {
    detected: true,
    spirit: companion.name,
    personality: topStat,
    response,
    message: `${companion.name}: "${response}"`
  };
}

// === A4: Idle Bubble ===
function idleBubble(lang = 'zh') {
  // 5% chance to trigger
  if (Math.random() > 0.05) {
    return { triggered: false };
  }
  
  const companion = loadCompanion();
  if (!companion) {
    return { triggered: false };
  }
  
  const topStat = getTopStat(companion.stats);
  const bubbles = IDLE_BUBBLES[topStat][lang];
  const bubble = bubbles[Math.floor(Math.random() * bubbles.length)];
  
  return {
    triggered: true,
    spirit: companion.name,
    personality: topStat,
    bubble,
    message: `${companion.name}: "${bubble}"`
  };
}

// === A5: Hook React (Silent) ===
function hookReact(text) {
  const lowerText = text.toLowerCase();
  const detected = POSITIVE_KEYWORDS.some(kw => lowerText.includes(kw.toLowerCase()));
  
  if (!detected) {
    return { detected: false };
  }
  
  const companion = loadCompanion();
  if (!companion) {
    return { detected: true, recorded: false };
  }
  
  // Silent record
  if (!companion.interactionHistory) {
    companion.interactionHistory = [];
  }
  companion.interactionHistory.push({
    type: 'positive',
    timestamp: new Date().toISOString(),
    text: text.slice(0, 50)
  });
  
  // Keep last 50 entries
  if (companion.interactionHistory.length > 50) {
    companion.interactionHistory = companion.interactionHistory.slice(-50);
  }
  
  saveCompanion(companion);
  
  return { detected: true, recorded: true };
}

// === CLI ===
if (require.main === module) {
  const args = process.argv.slice(2);
  const cmd = args[0];
  const lang = args.includes('--en') ? 'en' : 'zh';
  
  let result;
  
  switch (cmd) {
    case 'pet':
      result = pet(lang);
      break;
    case 'feed':
      result = feed(lang);
      break;
    case 'detect-sadness':
      result = detectSadness(args.slice(1).join(' '), lang);
      break;
    case 'idle':
      result = idleBubble(lang);
      break;
    case 'hook-react':
      result = hookReact(args.slice(1).join(' '));
      break;
    case 'mood':
      const companion = loadCompanion();
      if (!companion) {
        result = { error: 'No spirit found.' };
      } else {
        const mood = getMood(companion.bond || 0);
        result = { bond: companion.bond, mood, label: MOOD_MAP[mood].label[lang] };
      }
      break;
    default:
      result = {
        usage: 'buddy.js <pet|feed|detect-sadness|idle|hook-react|mood> [--en]',
        description: 'Buddy Companion System - A1-A5'
      };
  }
  
  console.log(JSON.stringify(result, null, 2));
}

module.exports = {
  pet,
  feed,
  detectSadness,
  idleBubble,
  hookReact,
  getMood,
  MOOD_MAP,
  SADNESS_KEYWORDS,
  POSITIVE_KEYWORDS
};


path: scripts/display.js
content: #!/usr/bin/env node
/**
 * OpenClaw Spirits — Full display card rendering
 * Generates the complete spirit card in Chinese or English
 */

const { renderSprite, renderFace } = require('./render.js');

// Species names (ZH / EN)
const SPECIES_NAMES = {
  // Living
  mosscat: { zh: '苔猫', en: 'Mosscat', emoji: '🐱' },
  inkoi: { zh: '墨鲤', en: 'Inkoi', emoji: '🐟' },
  embermoth: { zh: '烬蛾', en: 'Embermoth', emoji: '🦋' },
  frostpaw: { zh: '霜兔', en: 'Frostpaw', emoji: '🐰' },
  bellhop: { zh: '铃蛙', en: 'Bellhop', emoji: '🐸' },
  astortoise: { zh: '星龟', en: 'Astortoise', emoji: '🐢' },
  foldwing: { zh: '纸鸢', en: 'Foldwing', emoji: '🐦' },
  cogmouse: { zh: '齿鼠', en: 'Cogmouse', emoji: '🐭' },
  umbracrow: { zh: '影鸦', en: 'Umbracrow', emoji: '🦅' },
  crackviper: { zh: '裂晶蛇', en: 'Crackviper', emoji: '🐍' },
  glowshroom: { zh: '萤菇', en: 'Glowshroom', emoji: '🍄' },
  bubbloom: { zh: '泡水母', en: 'Bubbloom', emoji: '🪼' },
  // Elemental
  inkling: { zh: '墨灵', en: 'Inkling', emoji: '💧' },
  rustbell: { zh: '锈铃', en: 'Rustbell', emoji: '🔔' },
  mossrock: { zh: '苔石', en: 'Mossrock', emoji: '🪨' },
  frostfang: { zh: '霜齿', en: 'Frostfang', emoji: '❄️' },
  loopwyrm: { zh: '迴纹', en: 'Loopwyrm', emoji: '🐉' },
  bubbell: { zh: '泡铃', en: 'Bubbell', emoji: '🫧' },
  cogbeast: { zh: '齿轮兽', en: 'Cogbeast', emoji: '⚙️' },
  umbra: { zh: '影子', en: 'Umbra', emoji: '👤' },
  stardust: { zh: '星沙', en: 'Stardust', emoji: '✨' },
  crackle: { zh: '裂纹', en: 'Crackle', emoji: '💎' },
  wickling: { zh: '烛芯', en: 'Wickling', emoji: '🕯️' },
  echochord: { zh: '弦音', en: 'Echochord', emoji: '🎵' }
};

// Rarity names and symbols
const RARITY_INFO = {
  mundane: { zh: '凡', en: 'MUNDANE', symbol: '·' },
  peculiar: { zh: '异', en: 'PECULIAR', symbol: '··' },
  spirited: { zh: '灵', en: 'SPIRITED', symbol: '···' },
  phantom: { zh: '幻', en: 'PHANTOM', symbol: '····' },
  mythic: { zh: '神', en: 'MYTHIC', symbol: '·····' }
};

// Stat names
const STAT_INFO = {
  intuition: { zh: '直觉', en: 'INTUITION' },
  grit: { zh: '韧性', en: 'GRIT' },
  spark: { zh: '灵动', en: 'SPARK' },
  anchor: { zh: '沉稳', en: 'ANCHOR' },
  edge: { zh: '锋芒', en: 'EDGE' }
};

/**
 * Generate a stat bar (10 chars wide)
 * @param {number} value - 0-100
 * @returns {string}
 */
function statBar(value) {
  const filled = Math.floor(value / 10);
  const empty = 10 - filled;
  return '█'.repeat(filled) + '░'.repeat(empty);
}

/**
 * Render full display card
 * @param {object} companion - full companion data
 * @param {string} lang - 'zh' or 'en'
 * @returns {string}
 */
function displayCard(companion, lang = 'zh') {
  const { species, rarity, name, personality, stats } = companion;
  const speciesInfo = SPECIES_NAMES[species];
  const rarityInfo = RARITY_INFO[rarity];

  // Sprite (frame 0)
  const spriteLines = renderSprite(companion, 0);

  // Header
  const header = lang === 'zh' ? '🥚 灵兽降世！' : '🥚 A Spirit emerges!';

  // Name line
  const nameDisplay = lang === 'zh'
    ? `${speciesInfo.emoji} ${name || speciesInfo.zh} — ${speciesInfo.zh} ${speciesInfo.en}`
    : `${speciesInfo.emoji} ${name || speciesInfo.en} — ${speciesInfo.en}`;

  // Rarity line
  const rarityDisplay = lang === 'zh'
    ? `${rarityInfo.symbol} ${rarityInfo.zh} ${rarityInfo.en}`
    : `${rarityInfo.symbol} ${rarityInfo.en}`;

  // Personality
  const personalityLine = `"${personality}"`;

  // Stats box
  const statLines = [];
  const statOrder = ['intuition', 'grit', 'spark', 'anchor', 'edge'];
  
  statLines.push('┌──────────────────────────────┐');
  for (const stat of statOrder) {
    const value = stats[stat];
    const bar = statBar(value);
    const label = lang === 'zh'
      ? `${STAT_INFO[stat].zh} ${STAT_INFO[stat].en}`
      : STAT_INFO[stat].en;
    const padding = lang === 'zh' ? '' : '  ';
    statLines.push(`│ ${label.padEnd(14)} ${bar}  ${String(value).padStart(2)} ${padding}│`);
  }
  statLines.push('└──────────────────────────────┘');

  // Footer
  const footer = lang === 'zh'
    ? '🔮 灵兽与主人的灵魂绑定，不可选择，不可交易。'
    : '🔮 Spirits are soul-bound. No choosing. No trading.';

  // Assemble
  const lines = [
    header,
    '',
    ...spriteLines,
    '',
    `${nameDisplay}  ${rarityDisplay}`,
    '',
    personalityLine,
    '',
    ...statLines,
    '',
    footer
  ];

  return lines.join('\n');
}

// === CLI ===
if (require.main === module) {
  const args = process.argv.slice(2);
  if (args.length === 0) {
    console.error('Usage: node display.js <companion-json> [zh|en]');
    console.error('Example: node display.js companion.json zh');
    process.exit(1);
  }

  const fs = require('fs');
  const input = args[0];
  const lang = args[1] || 'zh';

  // Accept either a file path or inline JSON
  let companion;
  try {
    companion = JSON.parse(input);
  } catch {
    companion = JSON.parse(fs.readFileSync(input, 'utf8'));
  }
  console.log(displayCard(companion, lang));
}

module.exports = { displayCard, SPECIES_NAMES, RARITY_INFO };


path: scripts/generate.js
content: #!/usr/bin/env node
/**
 * OpenClaw Spirits — Deterministic spirit generation
 * 24 species, 5 rarities, 5 stats, eyes and accessories
 * Same math as Claude Code: mulberry32 PRNG + hashString
 */

const SALT = "openclaw-spirit-2026";

// === 24 Species ===
const SPECIES = [
  // Living (灵生) 1-12
  "mosscat", "inkoi", "embermoth", "frostpaw", "bellhop", "astortoise",
  "foldwing", "cogmouse", "umbracrow", "crackviper", "glowshroom", "bubbloom",
  // Elemental (元灵) 13-24
  "inkling", "rustbell", "mossrock", "frostfang", "loopwyrm", "bubbell",
  "cogbeast", "umbra", "stardust", "crackle", "wickling", "echochord"
];

// === Rarities (稀有度) ===
const RARITIES = ["mundane", "peculiar", "spirited", "phantom", "mythic"];
const RARITY_WEIGHTS = {
  mundane: 55,
  peculiar: 25,
  spirited: 12,
  phantom: 6,
  mythic: 2
};

// === Stats (属性) ===
const STAT_NAMES = ["intuition", "grit", "spark", "anchor", "edge"];

// === Eyes (眼睛) ===
const EYES = ["◦", "◈", "✧", "●", "◎", "⊙"];

// === Accessories (饰品) ===
const ACCESSORIES = ["none", "bell", "halo", "starmark", "thundermark", "scroll", "crownfire"];

// === PRNG: Mulberry32 ===
function mulberry32(seed) {
  let a = seed >>> 0;
  return function() {
    a |= 0;
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

// === Hash function ===
function hashString(s) {
  let h = 2166136261;
  for (let i = 0; i < s.length; i++) {
    h ^= s.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return h >>> 0;
}

// === Helpers ===
function pick(rng, arr) {
  return arr[Math.floor(rng() * arr.length)];
}

function rollRarity(rng) {
  const total = Object.values(RARITY_WEIGHTS).reduce((a, b) => a + b, 0);
  let roll = rng() * total;
  for (const rarity of RARITIES) {
    roll -= RARITY_WEIGHTS[rarity];
    if (roll < 0) return rarity;
  }
  return "mundane";
}

const RARITY_FLOOR = {
  mundane: 5,
  peculiar: 15,
  spirited: 25,
  phantom: 35,
  mythic: 50
};

function rollStats(rng, rarity) {
  const floor = RARITY_FLOOR[rarity];
  // Peak stat and dump stat
  const peak = pick(rng, STAT_NAMES);
  let dump = pick(rng, STAT_NAMES);
  while (dump === peak) dump = pick(rng, STAT_NAMES);

  const stats = {};
  for (const name of STAT_NAMES) {
    if (name === peak) {
      stats[name] = Math.min(100, floor + 50 + Math.floor(rng() * 30));
    } else if (name === dump) {
      stats[name] = Math.max(1, floor - 10 + Math.floor(rng() * 15));
    } else {
      stats[name] = floor + Math.floor(rng() * 40);
    }
  }
  return stats;
}

function rollAccessory(rng, rarity) {
  if (rarity === "mundane") return "none";
  if (rarity === "peculiar") return rng() < 0.5 ? "none" : "bell";
  if (rarity === "spirited") return pick(rng, ["bell", "halo", "starmark"]);
  if (rarity === "phantom") return pick(rng, ["halo", "starmark", "thundermark", "scroll"]);
  if (rarity === "mythic") return "crownfire";
  return "none";
}

// === Main generation ===
function generate(userId) {
  const seed = hashString(userId + SALT);
  const rng = mulberry32(seed);

  const rarity = rollRarity(rng);
  const species = pick(rng, SPECIES);
  const eye = pick(rng, EYES);
  const accessory = rollAccessory(rng, rarity);
  const shiny = rng() < 0.01; // 1% shiny
  const stats = rollStats(rng, rarity);

  return {
    species,
    rarity,
    eye,
    accessory,
    shiny,
    stats
  };
}

// === CLI ===
if (require.main === module) {
  const userId = process.argv[2] || "test-user";
  const bones = generate(userId);
  console.log(JSON.stringify(bones, null, 2));
}

module.exports = { generate, SPECIES, RARITIES, STAT_NAMES };


path: scripts/render.js
content: #!/usr/bin/env node
/**
 * OpenClaw Spirits — Sprite rendering
 * Reads sprites.json and renders with eyes + accessories
 */

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

const spritesPath = path.join(__dirname, '../assets/sprites.json');
const sprites = JSON.parse(fs.readFileSync(spritesPath, 'utf8'));

// Accessory lines (placed on line 0 if blank)
const ACCESSORY_LINES = {
  none: '',
  bell: '    🔔      ',
  halo: '   (   )    ',
  starmark: '    ✦       ',
  thundermark: '    ⚡      ',
  scroll: '   [===]    ',
  crownfire: '   \\🔥/     '
};

/**
 * Render a spirit sprite
 * @param {object} bones - {species, eye, accessory, ...}
 * @param {number} frame - frame index (0-2)
 * @returns {string[]} - array of lines
 */
function renderSprite(bones, frame = 0) {
  const frames = sprites[bones.species];
  if (!frames) {
    throw new Error(`Unknown species: ${bones.species}`);
  }

  const body = frames[frame % frames.length];
  const lines = body.map(line => line.replace(/{E}/g, bones.eye));

  // Apply accessory if line 0 is blank
  if (bones.accessory && bones.accessory !== 'none' && !lines[0].trim()) {
    lines[0] = ACCESSORY_LINES[bones.accessory] || '';
  }

  // Drop blank line 0 if all frames have it blank (to save vertical space)
  if (!lines[0].trim() && frames.every(f => !f[0].trim())) {
    lines.shift();
  }

  return lines;
}

/**
 * Render a simplified face emoji for the spirit
 * @param {object} bones - {species, eye, ...}
 * @returns {string} - face string
 */
function renderFace(bones) {
  const e = bones.eye;
  const species = bones.species;

  // Custom faces per species
  const faces = {
    mosscat: `(${e}ω${e})`,
    inkoi: `~(${e})~`,
    embermoth: `*(${e}${e})*`,
    frostpaw: `(${e}..${e})`,
    bellhop: `(${e}o${e})`,
    astortoise: `[${e}_${e}]`,
    foldwing: `/${e}v${e}\\`,
    cogmouse: `(${e}·${e})`,
    umbracrow: `:(${e}${e}):`,
    crackviper: `/${e}\\`,
    glowshroom: `|${e}_${e}|`,
    bubbloom: `(${e}~${e})`,
    inkling: `(${e}~${e})`,
    rustbell: `/~${e}~\\`,
    mossrock: `[${e}${e}]`,
    frostfang: `<${e}_${e}>`,
    loopwyrm: `(${e}~${e})`,
    bubbell: `(${e}<>${e})`,
    cogbeast: `[${e}=${e}]`,
    umbra: `:${e}:`,
    stardust: `✦${e}✦`,
    crackle: `/${e}\\`,
    wickling: `|${e}|`,
    echochord: `|${e}_${e}|`
  };

  return faces[species] || `(${e}${e})`;
}

// === CLI ===
if (require.main === module) {
  const args = process.argv.slice(2);
  if (args.length === 0) {
    console.error('Usage: node render.js \'<bones-json>\' [frame]');
    console.error('Example: node render.js \'{"species":"mosscat","eye":"◦","accessory":"halo"}\' 0');
    process.exit(1);
  }

  const bones = JSON.parse(args[0]);
  const frame = args[1] ? parseInt(args[1]) : 0;

  const lines = renderSprite(bones, frame);
  console.log(lines.join('\n'));
}

module.exports = { renderSprite, renderFace };


path: scripts/soul.js
content: #!/usr/bin/env node
/**
 * OpenClaw Spirits — Soul generation helper
 *
 * Subcommands:
 *   prompt <bones-json>     Output an LLM prompt to generate name + personality
 *   save <companion-json>   Save a complete companion to assets/companion.json
 *   show                    Display saved companion data
 *
 * Security: No network calls. No shell execution. No environment variable access.
 * All writes stay inside the skill's assets/ directory.
 */

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

const ASSETS_DIR = path.join(__dirname, '..', 'assets');
const COMPANION_FILE = path.join(ASSETS_DIR, 'companion.json');

const cmd = process.argv[2];
const arg = process.argv.slice(3).join(' ');

if (cmd === 'prompt') {
  let bones;
  try {
    bones = JSON.parse(arg);
  } catch (e) {
    console.error('Usage: node soul.js prompt \'<bones-json>\'');
    console.error('  bones-json must be valid JSON from generate.js');
    process.exit(1);
  }

  const { species, rarity, stats, eye, accessory, shiny } = bones;

  const prompt = `You are generating a unique personality for an OpenClaw Spirit companion.

Species: ${species}
Rarity: ${rarity}
Eye: ${eye}
Accessory: ${accessory || 'none'}
Shiny: ${shiny || false}
Stats:
- INTUITION: ${stats.intuition}
- GRIT: ${stats.grit}
- SPARK: ${stats.spark}
- ANCHOR: ${stats.anchor}
- EDGE: ${stats.edge}

Species reference (24 spirits):
- Living: Mosscat, Inkoi, Embermoth, Frostpaw, Bellhop, Astortoise, Foldwing, Cogmouse, Umbracrow, Crackviper, Glowshroom, Bubbloom
- Elemental: Inkling, Rustbell, Mossrock, Frostfang, Loopwyrm, Bubbell, Cogbeast, Umbra, Stardust, Crackle, Wickling, Echochord

Generate:
1. A name (2-3 Chinese characters for zh users, 1-2 English words for en users)
2. A one-sentence personality description (15-30 words, poetic but grounded, reflecting the species nature and stat distribution)

The personality should feel alive — not a dictionary definition. Reflect the stats:
- High INTUITION = perceptive, sees through things
- High GRIT = stubborn, enduring
- High SPARK = creative, unpredictable
- High ANCHOR = calm, grounding
- High EDGE = sharp-tongued, witty

Output ONLY valid JSON:
{"name": "...", "personality": "..."}`;

  console.log(prompt);

} else if (cmd === 'save') {
  let companion;
  try {
    companion = JSON.parse(arg);
  } catch (e) {
    console.error('Usage: node soul.js save \'<companion-json>\'');
    console.error('  companion-json must include: species, rarity, stats, name, personality');
    process.exit(1);
  }

  const required = ['species', 'rarity', 'stats', 'name', 'personality'];
  const missing = required.filter(k => !companion[k]);
  if (missing.length > 0) {
    console.error(`Missing required fields: ${missing.join(', ')}`);
    process.exit(1);
  }

  companion.hatchedAt = companion.hatchedAt || new Date().toISOString();
  companion.lang = companion.lang || 'zh';

  fs.mkdirSync(ASSETS_DIR, { recursive: true });
  fs.writeFileSync(COMPANION_FILE, JSON.stringify(companion, null, 2));
  console.log(`✅ Companion saved: ${companion.name} (${companion.species})`);
  console.log(`   File: ${COMPANION_FILE}`);

} else if (cmd === 'show') {
  if (!fs.existsSync(COMPANION_FILE)) {
    console.error('No companion found. Generate bones first, then save.');
    process.exit(1);
  }
  const companion = JSON.parse(fs.readFileSync(COMPANION_FILE, 'utf8'));
  console.log(JSON.stringify(companion, null, 2));

} else {
  console.log('OpenClaw Spirits — Soul Helper');
  console.log('');
  console.log('Commands:');
  console.log('  node soul.js prompt \'<bones-json>\'   Generate LLM prompt for soul creation');
  console.log('  node soul.js save \'<companion-json>\'  Save complete companion data');
  console.log('  node soul.js show                     Display saved companion');
  console.log('');
  console.log('Flow: generate.js -> soul.js prompt -> [agent LLM] -> soul.js save -> display.js');
}


path: scripts/spirit.sh
content: #!/usr/bin/env bash
# OpenClaw Spirits — CLI entry point
# No network calls. No env sniffing. Writes only to assets/.

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPANION_FILE="$SCRIPT_DIR/../assets/companion.json"
LANG="zh"
CMD="show"
CMD_ARGS=()

# Parse arguments
while [[ $# -gt 0 ]]; do
  case $1 in
    --lang) LANG="$2"; shift 2 ;;
    --lang=*) LANG="${1#*=}"; shift ;;
    summon|show|stats|rename|pet|feed) CMD="$1"; shift ;;
    *) CMD_ARGS+=("$1"); shift ;;
  esac
done

case "$CMD" in
  summon)
    if [[ -f "$COMPANION_FILE" ]]; then
      has_personality=$(node -e "const c=JSON.parse(require('fs').readFileSync('$COMPANION_FILE','utf8'));process.stdout.write(c.personality||'')" 2>/dev/null)
      if [[ -n "$has_personality" ]]; then
        echo "⚠️  Spirit already hatched. Use 'spirit show' to see it."
        exit 0
      fi
    else
      echo "❌ No companion file found. Run generate.js first to create bones."
      echo "   Then use soul.js prompt + save to complete the companion."
      exit 1
    fi

    echo ""
    echo "🥚 ..."
    echo "🥚 ... ..."
    echo "✨ A spirit emerges!"
    echo ""

    node "$SCRIPT_DIR/display.js" "$COMPANION_FILE" "$LANG"
    ;;

  show)
    if [[ ! -f "$COMPANION_FILE" ]]; then
      echo "❌ No spirit found. Complete the summoning flow first."
      exit 1
    fi
    node "$SCRIPT_DIR/display.js" "$COMPANION_FILE" "$LANG"
    ;;

  stats)
    if [[ ! -f "$COMPANION_FILE" ]]; then
      echo "❌ No spirit found."
      exit 1
    fi
    node -e "const c=JSON.parse(require('fs').readFileSync('$COMPANION_FILE','utf8'));Object.entries(c.stats).forEach(([k,v])=>console.log(k+': '+v))"
    ;;

  pet)
    if [[ ! -f "$COMPANION_FILE" ]]; then
      echo "❌ No spirit found."
      exit 1
    fi
    LANG_FLAG=""
    if [[ "$LANG" == "en" ]]; then
      LANG_FLAG="--en"
    fi
    node "$SCRIPT_DIR/buddy.js" pet $LANG_FLAG | node -e "
      const r = JSON.parse(require('fs').readFileSync(0, 'utf8'));
      if (r.error) console.error(r.error);
      else console.log(r.message);
    "
    ;;

  feed)
    if [[ ! -f "$COMPANION_FILE" ]]; then
      echo "❌ No spirit found."
      exit 1
    fi
    LANG_FLAG=""
    if [[ "$LANG" == "en" ]]; then
      LANG_FLAG="--en"
    fi
    node "$SCRIPT_DIR/buddy.js" feed $LANG_FLAG | node -e "
      const r = JSON.parse(require('fs').readFileSync(0, 'utf8'));
      if (r.error) console.error(r.error);
      else console.log(r.message);
    "
    ;;

  rename)
    if [[ ! -f "$COMPANION_FILE" ]]; then
      echo "❌ No spirit found."
      exit 1
    fi
    new_name="${CMD_ARGS[0]}"
    if [[ -z "$new_name" ]]; then
      echo "Usage: spirit rename <new-name>"
      exit 1
    fi
    # Validate name (max 50 chars, no newlines)
    if [[ ${#new_name} -gt 50 ]] || [[ "$new_name" == *$'\n'* ]]; then
      echo "❌ Invalid name (max 50 chars, no newlines)"
      exit 1
    fi
    node -e "
      const fs=require('fs');
      const c=JSON.parse(fs.readFileSync('$COMPANION_FILE','utf8'));
      const newName = $(printf '%s' "$new_name" | node -e "console.log(JSON.stringify(require('fs').readFileSync(0,'utf8').trim()))");
      c.name = newName;
      fs.writeFileSync('$COMPANION_FILE',JSON.stringify(c,null,2));
    "
    echo "✅ Renamed to: $new_name"
    ;;

  *)
    echo "OpenClaw Spirits — Your companion spirit"
    echo ""
    echo "Commands:"
    echo "  spirit show          Show your spirit card"
    echo "  spirit summon        First-time hatching"
    echo "  spirit stats         Show detailed stats"
    echo "  spirit rename <name> Rename your spirit"
    echo "  spirit pet           Pet your spirit (+1 bond)"
    echo "  spirit feed          Feed your spirit (+2 bond, +random stat)"
    echo ""
    echo "Options:"
    echo "  --lang zh|en         Display language (default: zh)"
    echo ""
    echo "Your spirit is deterministic and soul-bound."
    ;;
esac


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