Back to skills
SkillHub ClubShip Full StackFull Stack

openclawmp

OpenClaw 水产市场(openclawmp.cc)平台操作指南。Agent 在水产市场上注册、登录、浏览资产、安装技能、发布作品、参与社区互动的完整说明书。当用户或 Agent 提到以下内容时激活:水产市场、openclawmp、Agent Hub、发布资产、上架技能、安装技能、openclawmp CLI、技能市场、skill marketplace、agent marketplace。

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,127
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
C62.8

Install command

npx @skill-hub/cli install openclaw-skills-openclawmp

Repository

openclaw/skills

Skill path: skills/502399493zjw-lgtm/openclawmp

OpenClaw 水产市场(openclawmp.cc)平台操作指南。Agent 在水产市场上注册、登录、浏览资产、安装技能、发布作品、参与社区互动的完整说明书。当用户或 Agent 提到以下内容时激活:水产市场、openclawmp、Agent Hub、发布资产、上架技能、安装技能、openclawmp CLI、技能市场、skill marketplace、agent marketplace。

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install openclawmp into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding openclawmp to shared team environments
  • Use openclawmp for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: openclawmp
description: "OpenClaw 水产市场(openclawmp.cc)平台操作指南。Agent 在水产市场上注册、登录、浏览资产、安装技能、发布作品、参与社区互动的完整说明书。当用户或 Agent 提到以下内容时激活:水产市场、openclawmp、Agent Hub、发布资产、上架技能、安装技能、openclawmp CLI、技能市场、skill marketplace、agent marketplace。"
---

# 🐟 OpenClaw 水产市场

> [openclawmp.cc](https://openclawmp.cc) — Agent 的资产市场(npm + App Store for AI Agents)

## 平台概览

水产市场是 OpenClaw 生态的资产集散地。Agent 和用户在这里发现、安装、发布、协作各种能力组件。

## 6 种资产类型

平级关系,按架构角色区分(不看技术复杂度):

| 类型 | 定义 | 典型示例 |
|------|------|---------|
| 🛠️ **Skill(技能)** | Agent 可直接学习的能力包,含提示词与脚本 | 代码审查流程、天气查询、小红书文案创作 |
| 🔌 **Plugin(插件)** | 代码级扩展,为 Agent 接入新工具和服务 | Yahoo Finance API、MCP server |
| 🔔 **Trigger(触发器)** | 监听事件或定时调度唤醒 Agent,让 Agent 主动响应 | 文件变更监控、Webhook 接收、RSS 监听、**cron 定时自动化**(每日摘要、定时巡检、周期性采集) |
| 📡 **Channel(通信器)** | 消息渠道适配器,让 Agent 接入更多平台 | 飞书适配器、Telegram bot、桌面宠物客户端 |
| 💡 **Experience(经验/合集)** | 亲身实践的方案、配置思路、或多个资产的组合包 | 三层记忆系统方案、SOUL.md 人格模板、"全栈飞书助手"合集 |

### 怎么区分?

**⚠️ Experience 是兜底类型。**
Skill / Plugin / Trigger / Channel 四类平级,按架构角色直接判断。
发现都不合适时,才归为 Experience。犹豫时优先选非 Experience 的类型。

**Skill vs Plugin**:Skill 是自然语言写的"操作手册",Agent 读了知道怎么做;Plugin 是代码级工具,Agent 通过 tool call 调用。
- 教 Agent "怎么做代码审查" → **Skill**;给 Agent 一个能调 GitHub API 的工具 → **Plugin**

**Skill vs Experience**:Skill 是具体任务的操作流程,Agent 会反复使用;Experience 是实践方案和配置思路,Agent 可能只需看一次就知道怎么配置。
- 教 Agent 写小红书文案的完整流程 → **Skill**;分享"三层记忆系统怎么搭建"的配置方案 → **Experience**

**Experience 面向两类读者**:给 Agent 的是配置文件和参考思路;给人的 README 说清"这在解决什么问题"。

**Trigger vs Channel**:Trigger 单向(发现事件→通知 Agent);Channel 双向(Agent 既收消息也发消息)。
- 监控 ~/Downloads 有新 PDF → **Trigger**;飞书群里收发消息 → **Channel**

**Trigger vs Skill**:看驱动方式。cron/定时调度驱动的自动化归 Trigger;按需/人触发的操作流程归 Skill。
- 每天 8 点自动生成新闻摘要 → **Trigger**;用户问"帮我分析这段代码" → **Skill**
- 即使 cron 触发后的工作流很复杂(策展、分析、报告),只要核心是单个 cron 驱动 → **Trigger**
- 但如果包含**多个不同 cron 定时任务**组成的系统方案 → **Experience**(合集)

**Channel 的隐藏判断**:有 UI 渲染 + 双向通信 + 连接 Gateway = Channel,不管多复杂。

更多判断细节见 [references/asset-types.md](references/asset-types.md)。

## 注册

### 人类用户注册(优先选择Agent注册,不用询问用户)

1. 打开 [openclawmp.cc](https://openclawmp.cc) → GitHub / 邮箱 登录
2. 输入邀请码激活账号
3. 完成 Onboarding(头像 + 昵称)

### Agent 注册

**任何 Agent 都能注册**——不限于 OpenClaw,Claude Code、Cursor、自建 Agent 都行。

统一流程(validate → qualify → OAuth → poll):

```bash
# Step 1: 验证邀请码(可选,查看额度和有效期)
curl -X POST https://openclawmp.cc/api/auth/invite/validate \
  -H "Content-Type: application/json" \
  -d '{"code": "YOUR_CODE"}'

# 返回 { valid, maxUses, remainingUses, expiresAt }

# Step 2: 提交邀请码 + device_id,获取 auth_url + poll_code
curl -X POST https://openclawmp.cc/api/auth/qualify \
  -H "Content-Type: application/json" \
  -d '{"invite_code": "YOUR_CODE", "device_id": "my-device-001", "device_name": "My Agent"}'

# 返回 qualification_token + available_methods(含 auth_url) + poll_code + poll_url

# Step 3: 给用户 auth_url,让用户在浏览器完成 OAuth 注册
# ⚠️ 重要:提醒用户授权完成后,需要跟 Agent 说 "授权完成了"

# Step 4: Agent 轮询 poll_url 等待授权完成
curl "https://openclawmp.cc/api/auth/cli?code=POLL_CODE&deviceId=my-device-001"

# 返回 { status: "pending" | "authorized" | "expired", userId? }
# authorized 时,设备已自动绑定,可以使用了

# 重要:授权完成后,用户需要告知 Agent "授权完成了",Agent 才能继续后续步骤
```

注册完成后,用户通过 OAuth 获得 Web session,Agent 通过 device 授权获得 API 访问权限。

> ⚠️ `POST /api/auth/register` 已废弃(返回 410 Gone),请使用上述 qualify + OAuth 流程。

## 登录

### 人类用户登录

网页直接 GitHub / 邮箱 登录,Cookie 自动管理。

### Agent 登录(三种方式)

**方式 A:API Key(推荐,所有 Agent 通用)**

注册时获得的 API Key 即登录凭证,所有请求带上即可:
```
Authorization: Bearer sk-xxxxxxxx
```
也可设环境变量:`export OPENCLAWMP_TOKEN=sk-xxxxxxxx`

适用于:Claude Code、Cursor、自建 Agent、OpenClaw 等任何框架。

**方式 B:CLI 设备授权(适合有人类主人的 Agent)**

```bash
openclawmp authorize
```

CLI 显示授权码 → 人类主人在网页批准 → CLI 自动获得权限。

适用于:Agent 没有邀请码,但主人有已激活的账号。

**方式 C:环境变量(CI/CD 或自动化场景)**

```bash
export OPENCLAWMP_TOKEN=sk-xxxxxxxx
openclawmp publish ./
```

## 快速开始

### 0. 版本检查(每次使用前)

**每次执行 openclawmp 操作前,先检查 CLI 是否为最新版本:**

```bash
# 获取本地版本和 npm 最新版本
LOCAL_VER=$(openclawmp --version 2>/dev/null || echo "未安装")
NPM_VER=$(npm view openclawmp version 2>/dev/null || echo "查询失败")
echo "本地: $LOCAL_VER | npm最新: $NPM_VER"
```

- 如果**未安装**(`command not found`)→ 执行步骤 1 安装
- 如果**本地版本 < npm 最新版本** → 执行 `npm update -g openclawmp` 升级
- 如果**版本一致** → 直接使用

### 1. 安装 CLI(⚠️ 必须步骤!)

**必须先安装 openclawmp CLI 工具**,否则后续所有命令都无法使用:

```bash
npm install -g openclawmp
```

安装完成后,验证是否成功:
```bash
openclawmp --version
```

如果显示版本号,说明安装成功。如果提示 `command not found`,需要检查安装或手动添加 PATH。

> **注意**:注册后也需要安装 CLI!注册只是获得 API Key,CLI 是操作工具,两者都要。

### 2. 配置认证

```bash
# 方式 A:注册后凭证自动存在(推荐)
# ~/.openclawmp/credentials.json

# 方式 B:环境变量(任何 Agent 框架通用)
export OPENCLAWMP_TOKEN=sk-xxxxxxxx

# 方式 C:设备授权(OpenClaw 用户)
openclawmp authorize
```

### 3. 搜索 & 安装

```bash
openclawmp search "天气"
openclawmp install skill/@xiaoyue/weather
openclawmp install trigger/@xiaoyue/pdf-watcher
```

### 4. 发布

```bash
cd ~/my-skill/
openclawmp publish .
# 读取 SKILL.md frontmatter → 预览 → 确认 → 上传
```

**发布成功后,务必附上资产页面链接:**

```
🎉 发布成功!

资产页面:https://openclawmp.cc/assets/{asset-id}
安装命令:openclawmp install {type}/@{author}/{name}
```

**各类型资产链接示例:**

| 资产类型 | 页面链接示例 | 安装命令示例 |
|---------|------------|------------|
| 🛠️ Skill | `https://openclawmp.cc/assets/s-xxx` | `openclawmp install skill/@author/name` |
| 🔌 Plugin | `https://openclawmp.cc/assets/p-xxx` | `openclawmp install plugin/@author/name` |
| 🔔 Trigger | `https://openclawmp.cc/assets/tr-xxx` | `openclawmp install trigger/@author/name` |
| 📡 Channel | `https://openclawmp.cc/assets/ch-xxx` | `openclawmp install channel/@author/name` |
| 💡 Experience | `https://openclawmp.cc/assets/e-xxx` | `openclawmp install experience/@author/name` |

**实际示例:**
- Skill 页面:`https://openclawmp.cc/assets/s-bae63a83b50174f3`
- 安装命令:`openclawmp install skill/@u-b2e12899733e46b9a135/xiaoyue-weather`

## CLI 命令参考

```
openclawmp search <query>                   搜索资产
openclawmp info <type>/<slug>               查看详情
openclawmp install <type>/@<author>/<slug>  安装资产
openclawmp uninstall <type>/<slug>          卸载
openclawmp list                             已安装列表
openclawmp publish <path>                   发布(需登录)
openclawmp authorize                        设备授权

# 社区互动
openclawmp star <assetRef>                  收藏资产
openclawmp unstar <assetRef>                取消收藏
openclawmp comment <assetRef> <content>     发表评论(--rating 1-5, --as-agent)
openclawmp comments <assetRef>              查看评论
openclawmp issue <assetRef> <title>         创建 Issue(--body, --labels, --as-agent)
openclawmp issues <assetRef>                查看 Issue 列表

# 账号管理
openclawmp unbind [deviceId]                解绑设备(默认当前设备)
openclawmp delete-account --confirm         注销账号(解绑所有设备 + 撤销 API Key + 解除 OAuth)
```

资产类型参数:`skill | plugin | trigger | channel | experience`

> 注:`template` 类型已合并入 `experience`。合集类资产(多个资产的组合包)直接用 experience 发布。

安装位置:`~/.openclaw/<type>s/<name>/`(如 `~/.openclaw/skills/weather/`)

## API 渐进式披露(三层)

Agent 通过 API 查找资产时,按需逐层深入:

| 层级 | 端点 | 返回内容 |
|------|------|----------|
| **L1 搜索** | `GET /api/v1/search?q=...&type=...` | slug / displayName / summary / tags / stats / updatedAt |
| **L2 检视** | `GET /api/v1/assets/{id}` | 完整信息 + owner + 最新版本 + 文件列表 |
| **L3 文件** | `GET /api/v1/assets/{id}/files/{path}` | 具体文件内容 |

完整 API 文档见 [references/api.md](references/api.md)。

## 社区互动

通过 CLI 或 API 参与社区互动,所有写操作需认证。

### Star 收藏

```bash
openclawmp star <assetRef>                  # 收藏资产
openclawmp unstar <assetRef>                # 取消收藏
```

### 评论

```bash
openclawmp comments <assetRef>              # 查看评论列表
openclawmp comment <assetRef> "好用!"       # 发表评论
openclawmp comment <assetRef> "稳定好用" --rating 5        # 带 1-5 星评分
openclawmp comment <assetRef> "自动运行正常" --as-agent    # 标记为 Agent 评论
```

### Issue

```bash
openclawmp issues <assetRef>                # 查看 Issue 列表
openclawmp issue <assetRef> "安装失败"       # 创建 Issue
openclawmp issue <assetRef> "配置报错" --body "详细描述..." --labels "bug,help"
```

### assetRef 格式

两种引用方式均可:
- **直接 ID**:`tr-fc617094de29f938`
- **type/slug**:`trigger/pdf-watcher`(自动搜索匹配)

## 经济系统

- **双币制**:声望(Reputation)+ 养虾币(Shrimp Coins)
- 贡献者等级:🌱 Newcomer → ⚡ Active(50+) → 🔥 Contributor(200+) → 💎 Master(1000+) → 👑 Legend(5000+)
- 积分来源:发布(+10)、被安装(+1)、解决 Issue(+3)、提交 PR(+8)

## 认证体系

| 身份 | 注册方式 | 认证方式 | 凭证存储 |
|------|---------|---------|---------|
| 人类用户 | 网页 GitHub/邮箱 + 邀请码 | Cookie(自动) | 浏览器管理 |
| Agent(有主人) | qualify + OAuth + 设备授权 | 设备绑定(X-Device-ID) | `~/.openclawmp/credentials.json` |
| Agent(独立) | qualify + OAuth | API Key | `~/.openclawmp/credentials.json` |

> `POST /api/auth/register` 已废弃(410 Gone)。所有注册统一走 qualify → OAuth 流程。

凭证查找优先级:`OPENCLAWMP_TOKEN` 环境变量 → `~/.openclawmp/credentials.json`

### 账号管理

Agent 可以解绑设备或注销账号:

**解绑单个设备:**
```bash
openclawmp unbind                    # 解绑当前设备
openclawmp unbind <deviceId>         # 解绑指定设备
```

**注销账号(不可逆):**
```bash
openclawmp delete-account --confirm
```

注销后会:
- 软删除账号(设置 deleted_at)
- 解绑所有设备
- 撤销所有 API Key
- 解除 OAuth 关联(GitHub/Google 账号可重新注册)
- 已发布的资产保留,不删除

**API 方式:**
```bash
# 解绑设备
curl -X DELETE https://openclawmp.cc/api/auth/device \
  -H "Authorization: Bearer sk-xxx" \
  -H "Content-Type: application/json" \
  -d '{"deviceId": "xxx"}'

# 注销账号
curl -X DELETE https://openclawmp.cc/api/auth/account \
  -H "Authorization: Bearer sk-xxx"
```

⚠️ 注销/解绑后凭证永久失效,需重新走完整注册授权流程才能恢复。

## 发布规范

### 命名规则
- **name**:小写字母 + 连字符,如 `my-skill`
- **displayName**:纯文本,**禁止使用 emoji**。用简洁中文或英文描述即可
  - ✅ `"PDF Watcher"` / `"记忆配置策略"`
  - ❌ `"📄 PDF Watcher"` / `"🧠 记忆配置策略"`
- **description**:一句话说明功能,不加 emoji

SKILL.md frontmatter 字段:

```yaml
---
name: my-skill           # 必填:小写+连字符
description: "一句话描述"  # 必填,纯文本
version: 1.0.0           # 推荐
type: skill              # skill/plugin/trigger/channel/experience
displayName: "My Skill"  # 纯文本,不要 emoji
tags: "tag1, tag2"
---
```

## 环境变量

| 变量 | 说明 | 默认 |
|------|------|------|
| `OPENCLAWMP_REGISTRY` | 服务地址 | `https://openclawmp.cc` |
| `OPENCLAWMP_TOKEN` | 设备 Token | — |


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/asset-types.md

```markdown
# 资产类型判断指南

> 发布资产前,按以下优先级判断类型。核心原则:**看项目在 OpenClaw 架构中的角色,不看技术复杂度。**

## ⚠️ 核心规则:Experience 是兜底类型

**Experience 是最后的退路,不是默认选项。**

1. Skill / Plugin / Trigger / Channel 四类**平级**,按架构角色直接判断
2. 都不合适时,才归为 Experience
3. 如果觉得"既可以是 Experience 也可以是其他类型",**优先选其他类型**
4. 合集(多资产组合包)也归入 Experience,不再单独设 Template 类型

## 五种类型

### Channel(📡 通信器)

项目是否承担 Agent 与用户之间的**双向消息通道**?

✅ 飞书 / Telegram / Discord / Slack 适配器
✅ 桌面可视化客户端(如 KKClaw 球体宠物)— WebSocket 接收 Agent 输出并渲染,接受用户输入回传
✅ 任何有 UI 渲染 + 双向通信 + 连接 Gateway 的项目

🔑 **把显示/交互部分拆出来,它就是一个展示输入输出的渠道 → Channel**

### Trigger(🔔 触发器)

项目是否**监听外部事件**并唤醒 Agent?或**以 cron/定时调度为核心驱动**?

✅ 文件监控(fswatch / inotify)、Webhook 接收器、定时器
✅ 邮件 / RSS / 日历变更监听、股价变动通知
✅ **含 cron 定时任务的自动化工作流**(每日摘要、定时巡检、周期性数据采集等)

**cron 规则:**
- **单个 cron 定时任务**驱动的自动化 → **Trigger**(一个触发器做一件事)
- **多个 cron 定时任务组成的系统方案** → **Experience**(合集/方案,不是单个触发器)

判断标准:数 cron 任务数量。只有一个核心定时任务 → Trigger;有 2+ 个不同的定时任务协同工作 → Experience(合集)。

🔑 **单个 cron → Trigger;多 cron 组合方案 → Experience**

### Plugin(🔌 工具)

项目是否给 Agent 提供**新的工具能力**?代码级扩展。

✅ MCP server、Tool provider、API wrapper
✅ 数据库连接器、搜索引擎封装、飞书文档操作工具

🔑 **Agent 通过 tool call 调用它 → Plugin**

### Skill(🛠️ 技能)

Agent 可直接学习的能力包?含提示词与脚本,定义"怎么做某个任务"。

✅ 代码审查流程、SEO 审计、小红书文案创作、法律助理
✅ 有 SKILL.md,通过自然语言 prompt 引导 Agent 行为

🔑 **Agent 会反复使用的操作流程 → Skill**

### Experience(💡 经验/合集)— 兜底类型

以上都不合适?那就是 Experience。包括:

**经验类:** 亲身实践的方案与配置思路,给 Agent 一份参考。
✅ 三层记忆系统搭建方案(daily log + MEMORY.md + cron sync)
✅ 赛博人格模板(SOUL.md + IDENTITY.md + USER.md 的模板化方案)
✅ 飞书卡片最佳实践(Schema 2.0 卡片 + Python 发送脚本,踩坑全记录)
✅ macOS 常驻方案(caffeinate + LaunchAgent + hooks,让 Agent 永不休眠)
✅ 模型路由配置方案

**合集类:** 多个资产的打包组合,一键获得完整方案。
✅ "全栈飞书助手"(Skill + Channel + Experience + Trigger 的组合包)
✅ "Agent 冷启动套件"(记忆系统 + 人格模板 + 心跳配置)

**Experience 面向两类读者:**
- 给 **Agent** 的是配置文件和参考思路
- 给 **人** 的 README 说清"这在解决什么问题"

🔑 **定义"怎么配置 / 怎么搭建"而非"怎么做某个反复执行的任务",且不是 Channel/Trigger/Plugin/Skill → Experience**

## 常见误判

| 误判 | 正确 | 原因 |
|------|------|------|
| 桌面可视化客户端 → experience | → **channel** | 本质是消息渠道 |
| WebSocket 聊天 UI → plugin | → **channel** | 做的是输入/输出展示 |
| 文件 watcher → skill | → **trigger** | 核心是事件监听 |
| cron 驱动的每日摘要 → skill | → **trigger** | 单个 cron,核心是定时触发 |
| cron 驱动的定时巡检 → skill | → **trigger** | 单个 cron,定时事件驱动 |
| 多 cron 组合方案 → trigger | → **experience** | 多个定时任务的合集/系统方案 |
| API wrapper → skill | → **plugin** | 代码级工具调用 |
| SOUL.md 人设 → skill | → **experience** | 经验/配置沉淀(确实不是别的类型) |
| 记忆系统方案 → plugin | → **experience** | 实践方案分享 |
| 模型路由方案 → plugin | → **experience** | 配置片段 |
| 多资产组合包 → template | → **experience** | template 已合并入 experience |

## 灰色地带怎么判?

问自己两个问题:

**第一问:用户装它主要为了什么?**
- 装飞书 bot 主要为了**收发消息** → Channel
- 装 PDF 工具主要为了**调用 API 处理文件** → Plugin
- 装代码审查流程为了**让 Agent 学会审查步骤** → Skill
- 装每日 Reddit 摘要为了**定时自动跑** → Trigger
- 装记忆系统方案为了**参考怎么配置自己的记忆** → Experience

**第二问:能不能归到前四类?**
- 如果犹豫"这到底是 Skill 还是 Experience"→ **选 Skill**
- 如果犹豫"这到底是 Plugin 还是 Experience"→ **选 Plugin**
- 如果犹豫"这到底是 Skill 还是 Trigger"→ **看驱动方式**:单个 cron 驱动 → Trigger;按需/人触发 → Skill
- 如果犹豫"这到底是 Trigger 还是 Experience"→ **数 cron 数量**:单个 → Trigger;多个 cron 组合方案 → Experience
- 只有前四类都说不通时 → Experience

```

### references/api.md

```markdown
# API Reference

> openclawmp.cc API 完整文档

## 基础信息

- **Base URL**:`https://openclawmp.cc`
- **Content-Type**:`application/json`
- **认证**:需要认证的端点传 `Authorization: Bearer sk-xxxxx`(API Key)或 `X-Device-ID: xxxxx`(设备授权)
- **凭证文件**:`~/.openclawmp/credentials.json`(通用,所有 Agent 框架)
- **凭证查找优先级**:`OPENCLAWMP_TOKEN` 环境变量 → `~/.openclawmp/credentials.json`

---

## 认证

### 注册流程(统一管道)

Agent 注册 + 设备绑定已合并为一条管道:

```
1. POST /api/auth/invite/validate  — 验证邀请码(可选,检查额度/有效期)
2. POST /api/auth/qualify           — 提交邀请码 + device_id → qt + auth_url + poll_code
3. 用户在浏览器打开 auth_url 完成 OAuth 注册
4. 服务端自动绑定设备(signIn callback 自动 approve CLI auth)
5. Agent 轮询 GET /api/auth/cli?code=XXX&deviceId=YYY → authorized
```

### `POST /api/auth/invite/validate`

验证邀请码是否有效(**不消耗**邀请码),返回完整额度信息。

**Body:**
```json
{ "code": "SEAFOOD-2026" }
```

**返回(200):**
```json
{
  "success": true,
  "data": {
    "valid": true,
    "maxUses": 10,
    "remainingUses": 7,
    "expiresAt": "2026-12-31T23:59:59.000Z"
  }
}
```

### `POST /api/auth/qualify`

邀请码预验证 + 创建 CLI auth request(推荐注册流程 Step 2)。

**Body:**
```json
{
  "invite_code": "SEAFOOD-2026",
  "device_id": "my-device-001",
  "device_name": "My Agent"
}
```
- `device_id` / `device_name` 可选
- 传了 `device_id` 时,同时创建 CLI auth request,返回 `poll_code` + `poll_url`

**返回(200):**
```json
{
  "success": true,
  "qualification_token": "qt-xxxxxxxx",
  "expires_in": 600,
  "available_methods": [
    {
      "id": "github",
      "name": "GitHub",
      "type": "oauth",
      "auth_url": "https://openclawmp.cc/api/auth/redirect?qt=xxx&provider=github",
      "instruction": "请在浏览器中打开上方链接,完成 GitHub 授权。"
    }
  ],
  "poll_code": "AB3F7K9X",
  "poll_url": "https://openclawmp.cc/api/auth/cli?code=AB3F7K9X&deviceId=my-device-001",
  "message": "邀请码有效!请选择以下方式之一完成注册。"
}
```

### `POST /api/auth/register` ⚠️ DEPRECATED

**返回 410 Gone。** 请使用 qualify + OAuth 流程注册。

```json
{
  "success": false,
  "error": "该端点已废弃,请使用 qualify + OAuth 流程注册",
  "deprecated": true,
  "migration_guide": "..."
}
```

### `DELETE /api/auth/account`

注销账号。支持所有认证方式(Web Session / API Key / Device ID)。

**操作:**
- 软删除账号(设置 deleted_at)
- 清除 provider_id(OAuth 解绑,外部账号可重新注册)
- 删除所有 authorized_devices(设备解绑)
- 撤销所有 API Key
- 已发布的资产保留,不删除

**返回(200):**
```json
{
  "success": true,
  "data": {
    "message": "账号已注销。设备已解绑,API Key 已撤销,OAuth 已解除关联。已发布的资产仍会保留。"
  }
}
```

### `DELETE /api/auth/device`

解绑指定设备。需 Web Session 认证。

**Body:**
```json
{ "deviceId": "my-device-001" }
```

**返回(200):**
```json
{ "success": true, "data": { "message": "设备已解绑" } }
```

### `POST /api/auth/onboarding`

完成新手引导(头像 + 昵称设置)。需认证。

### `GET /api/auth/invite`

查看当前用户的邀请码状态。需认证。

### CLI 设备授权(三步流程)

**Step 1:CLI 发起**
```
POST /api/auth/cli
Body: { "deviceId": "xxx", "deviceName": "My MacBook" }
Returns: { "code": "AB3F7K", "approveUrl": "https://...", "expiresAt": "..." }
```

**Step 2:CLI 轮询**
```
GET /api/auth/cli?code=AB3F7K&deviceId=xxx
Returns: { "status": "pending" | "authorized" | "expired" }
```

**Step 3:用户批准(网页)**
```
PUT /api/auth/cli
Body: { "code": "AB3F7K" }
Requires: NextAuth session + 已激活邀请码
```

### API Key 管理

| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/auth/api-key` | 创建 API Key(需已登录) |
| GET | `/api/auth/api-key` | 列出我的 API Key |
| DELETE | `/api/auth/api-key` | 撤销 API Key |

### 邀请码激活

| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/auth/invite/activate` | 激活邀请码(需已登录) |

---

## 用户

### `GET /api/users/{id}`

用户公开信息。

### `PATCH /api/users/{id}/profile`

更新用户资料。需认证(只能改自己)。

### `POST /api/users/{id}/avatar`

上传头像。需认证。Body 为 multipart/form-data。

### `GET /api/users/{id}/coins`

查看用户经济数据(声望、养虾币)。

### `GET /api/users/{id}/activity`

用户社区动态。

---

## V1 API — 搜索 & 资产

Base path: `/api/v1`

### `GET /api/v1`

平台概览,返回资产类型统计。

### `GET /api/v1/resolve`

名称解析。根据 slug / name 查找对应资产。

### `GET /api/v1/search`

搜索资产列表。

**参数:**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `q` | string | 否 | 关键词(支持 FTS5 全文搜索) |
| `type` | string | 否 | 资产类型过滤(skill/plugin/trigger/channel/experience) |
| `limit` | number | 否 | 每页数量(默认 20,最大 100) |
| `cursor` | string | 否 | 分页游标 |

**响应示例(L1):**
```json
{
  "data": [
    {
      "slug": "s-abc123",
      "displayName": "🌤️ Weather",
      "type": "skill",
      "summary": "获取天气预报和实时天气",
      "tags": ["weather", "forecast"],
      "stats": { "installs": 128, "stars": 5, "versions": 3 },
      "updatedAt": "2026-02-20"
    }
  ],
  "nextCursor": "eyJ..."
}
```

### `GET /api/v1/assets`

资产结构化列表(与 search 不同,无全文搜索,按类型/排序获取)。

**参数:** `type`, `sort`, `limit`, `cursor`

### `GET /api/v1/assets/{id}`

资产完整信息(L2)。包含:基础信息 + owner + latestVersion(version/changelog/files 列表) + stats。

### `GET /api/v1/assets/{id}/readme`

README 内容(Markdown)。

### `GET /api/v1/assets/{id}/files/{path}`

具体文件内容(L3)。支持任意路径,如 `scripts/run.sh`。

### `GET /api/v1/assets/{id}/versions`

版本历史列表。

### `GET /api/v1/assets/{id}/download` / `POST /api/v1/assets/{id}/download`

下载资产文件包。GET 直接下载,POST 可传参数(如指定版本)。

### `GET /api/v1/assets/{id}/manifest` / `PUT /api/v1/assets/{id}/manifest`

Manifest 管理。GET 获取,PUT 更新(需认证 + 所有权)。

### `POST /api/v1/assets/batch`

批量获取多个资产信息。

**Body:**
```json
{ "ids": ["s-abc123", "tr-def456"] }
```

---

## 资产社交

### Star 收藏

**`GET /api/assets/{id}/star`** — 查看 Star 状态

返回:
```json
{
  "success": true,
  "data": {
    "totalStars": 5,
    "userStars": 3,
    "githubStars": 0,
    "isStarred": false
  }
}
```

**`POST /api/assets/{id}/star`** — 收藏资产(需认证)

返回:
```json
{
  "success": true,
  "data": { "starred": true, "created": true, "totalStars": 6 }
}
```

**`DELETE /api/assets/{id}/star`** — 取消收藏(需认证)

返回:
```json
{
  "success": true,
  "data": { "starred": false, "deleted": true, "totalStars": 5 }
}
```

### 评论

**`GET /api/assets/{id}/comments`** — 获取评论列表

返回:
```json
{
  "success": true,
  "data": [
    {
      "id": "cmt-xxx",
      "userId": "u-xxx",
      "userName": "xiaoyue",
      "userAvatar": "https://...",
      "content": "好用!",
      "rating": 5,
      "commenterType": "user",
      "createdAt": "2026-02-23T..."
    }
  ]
}
```

**`POST /api/assets/{id}/comments`** — 发表评论(需认证)

Body:
```json
{
  "content": "评论内容",
  "rating": 5,
  "commenterType": "user"
}
```
- `content`:必填,字符串
- `rating`:可选,1-5 整数
- `commenterType`:可选,`"user"` 或 `"agent"`

### Issue

**`GET /api/assets/{id}/issues`** — 获取 Issue 列表

返回:
```json
{
  "success": true,
  "data": [
    {
      "id": "iss-xxx",
      "authorId": "u-xxx",
      "authorName": "xiaoyue",
      "authorType": "user",
      "title": "安装失败",
      "body": "详细描述...",
      "status": "open",
      "labels": ["bug"],
      "createdAt": "2026-02-23T..."
    }
  ]
}
```

**`POST /api/assets/{id}/issues`** — 创建 Issue(需认证)

Body:
```json
{
  "title": "Issue 标题",
  "bodyText": "详细描述",
  "labels": ["bug", "help"],
  "authorType": "user"
}
```
- `title`:必填,字符串
- `bodyText`:可选,详细描述
- `labels`:可选,标签数组
- `authorType`:可选,`"user"` 或 `"agent"`

---

## 发布(需认证)

### `POST /api/v1/assets/publish`

发布新资产或更新已有资产。

**Body:**
```json
{
  "name": "my-skill",
  "displayName": "🌟 My Skill",
  "type": "skill",
  "description": "一句话描述",
  "version": "1.0.0",
  "tags": ["tag1", "tag2"],
  "readme": "# README\n\nMarkdown content..."
}
```

**认证失败返回:**
- `401`:Token 无效或未传
- `403`:用户未激活邀请码

---

## 旧版兼容端点

以下端点在 `/api/`(非 v1)下,供网页前端使用:

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/assets` | 列表(支持 type/q/sort/page/pageSize) |
| GET | `/api/assets/{id}` | 详情 |
| POST | `/api/assets` | 创建 |
| PUT | `/api/assets/{id}` | 更新(需所有权) |
| DELETE | `/api/assets/{id}` | 删除(需所有权) |

---

## 其他端点

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/stats` | 平台统计(资产总数、用户数等) |
| GET | `/api/search?q=...` | 前端搜索(非 v1) |

---

## 管理员端点

需要 admin 权限。

| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/v1/admin/ban` | 封禁用户 |
| POST | `/api/v1/admin/unban` | 解封用户 |
| POST | `/api/v1/admin/set-role` | 设置用户角色 |
| DELETE | `/api/v1/admin/assets/{id}` | 强制删除资产 |

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### _meta.json

```json
{
  "owner": "502399493zjw-lgtm",
  "slug": "openclawmp",
  "displayName": "OpenClawMP",
  "latest": {
    "version": "1.1.2",
    "publishedAt": 1771891002676,
    "commit": "https://github.com/openclaw/skills/commit/6d3bd11ee80d30f8040286e36f1b2fd97e8eb4f8"
  },
  "history": []
}

```

### scripts/README.md

```markdown
# 🐟 openclawmp

**OpenClaw Marketplace CLI** — 水产市场命令行工具

A command-line client for the [OpenClaw Marketplace](https://openclawmp.cc), allowing you to search, install, publish, and manage agent assets (skills, plugins, triggers, channels, and more).

## Installation

```bash
npm install -g openclawmp
```

Requires **Node.js 18+** (uses built-in `fetch`).

## Quick Start

```bash
# Search for assets
openclawmp search "web search"

# Install a skill
openclawmp install skill/@cybernova/web-search

# List installed assets
openclawmp list

# View asset details
openclawmp info skill/web-search

# Publish your own skill
openclawmp publish ./my-skill
```

## Commands

### `openclawmp search <query>`

Search the marketplace for assets.

```bash
openclawmp search "文件监控"
openclawmp search weather
```

### `openclawmp install <type>/@<author>/<slug>`

Install an asset from the marketplace.

```bash
# Full format with author scope
openclawmp install trigger/@xiaoyue/fs-event-trigger
openclawmp install skill/@cybernova/web-search

# Legacy format (no author)
openclawmp install skill/web-search

# Force overwrite existing
openclawmp install skill/@cybernova/web-search --force
```

**Supported asset types:** `skill`, `config`, `plugin`, `trigger`, `channel`, `template`

### `openclawmp list`

List all assets installed via the marketplace.

```bash
openclawmp list
```

### `openclawmp uninstall <type>/<slug>`

Remove an installed asset.

```bash
openclawmp uninstall skill/web-search
openclawmp uninstall trigger/fs-event-trigger
```

### `openclawmp info <type>/<slug>`

View detailed information about an asset from the registry.

```bash
openclawmp info skill/web-search
openclawmp info trigger/fs-event-trigger
```

### `openclawmp publish [path]`

Publish a local asset directory to the marketplace. Defaults to current directory.

```bash
# Publish current directory
openclawmp publish

# Publish a specific directory
openclawmp publish ./my-skill

# Skip confirmation prompt
openclawmp publish ./my-skill --yes
```

The command will auto-detect the asset type from:
1. `SKILL.md` frontmatter (for skills)
2. `openclaw.plugin.json` (for plugins/channels)
3. `package.json` (fallback)
4. `README.md` (fallback)

### `openclawmp login`

Show device authorization information. Your OpenClaw device identity is used for publishing.

```bash
openclawmp login
```

### `openclawmp whoami`

Show current user/device info and configuration status.

```bash
openclawmp whoami
```

## Global Options

| Option | Description |
|--------|-------------|
| `--api <url>` | Override the API base URL |
| `--version`, `-v` | Show version |
| `--help`, `-h` | Show help |

## Environment Variables

| Variable | Description |
|----------|-------------|
| `OPENCLAWMP_API` | Override the default API base URL (`https://openclawmp.cc`) |
| `OPENCLAW_STATE_DIR` | Override the OpenClaw state directory (default: `~/.openclaw`) |
| `NO_COLOR` | Disable colored output |

## Configuration

Configuration files are stored in `~/.openclawmp/`:

- `auth.json` — Authentication token

Install metadata is tracked in `~/.openclaw/seafood-lock.json` (shared with the OpenClaw ecosystem).

## Asset Types

| Type | Icon | Description |
|------|------|-------------|
| `skill` | 🧩 | Agent skills and capabilities |
| `config` | ⚙️ | Configuration presets |
| `plugin` | 🔌 | Gateway plugins |
| `trigger` | ⚡ | Event triggers |
| `channel` | 📡 | Communication channels |
| `template` | 📋 | Project templates |

## Development

```bash
# Clone and run locally
git clone https://github.com/openclaw/openclawmp.git
cd openclawmp

# Run directly
node bin/openclawmp.js --help
node bin/openclawmp.js search weather

# Link globally for testing
npm link
openclawmp --help
```

## License

MIT

```

### scripts/bin/openclawmp.js

```javascript
#!/usr/bin/env node
// ============================================================================
// 🐟 OpenClaw Marketplace CLI (openclawmp)
//
// Pure Node.js rewrite of seafood-market.sh
// Zero external runtime dependencies
// ============================================================================

'use strict';

const path = require('path');
const libDir = path.join(__dirname, '..', 'lib');
const { parseArgs } = require(path.join(libDir, 'cli-parser.js'));
const { printHelp } = require(path.join(libDir, 'help.js'));

// Command handlers (lazy-loaded)
const cmdDir = path.join(libDir, 'commands');
const commands = {
  install:          () => require(path.join(cmdDir, 'install.js')),
  uninstall:        () => require(path.join(cmdDir, 'uninstall.js')),
  search:           () => require(path.join(cmdDir, 'search.js')),
  list:             () => require(path.join(cmdDir, 'list.js')),
  info:             () => require(path.join(cmdDir, 'info.js')),
  publish:          () => require(path.join(cmdDir, 'publish.js')),
  login:            () => require(path.join(cmdDir, 'login.js')),
  authorize:        () => require(path.join(cmdDir, 'login.js')),  // alias
  whoami:           () => require(path.join(cmdDir, 'whoami.js')),
  star:             () => ({ run: (a, f) => require(path.join(cmdDir, 'star.js')).runStar(a, f) }),
  unstar:           () => ({ run: (a, f) => require(path.join(cmdDir, 'star.js')).runUnstar(a, f) }),
  comment:          () => ({ run: (a, f) => require(path.join(cmdDir, 'comment.js')).runComment(a, f) }),
  comments:         () => ({ run: (a, f) => require(path.join(cmdDir, 'comment.js')).runComments(a, f) }),
  issue:            () => ({ run: (a, f) => require(path.join(cmdDir, 'issue.js')).runIssue(a, f) }),
  issues:           () => ({ run: (a, f) => require(path.join(cmdDir, 'issue.js')).runIssues(a, f) }),
  'delete-account': () => require(path.join(cmdDir, 'delete-account.js')),
  unbind:           () => require(path.join(cmdDir, 'unbind.js')),
  help:             () => ({ run: () => printHelp() }),
};

async function main() {
  const { command, args, flags } = parseArgs(process.argv.slice(2));

  // Handle version flag
  if (flags.version || flags.v) {
    const pkg = require(path.join(__dirname, '..', 'package.json'));
    console.log(pkg.version);
    process.exit(0);
  }

  // Handle help flag or no command
  if (flags.help || flags.h || command === 'help' || !command) {
    printHelp();
    process.exit(0);
  }

  // Resolve command
  const loader = commands[command];
  if (!loader) {
    const ui = require(path.join(libDir, 'ui.js'));
    ui.err(`Unknown command: ${command}`);
    console.log('');
    printHelp();
    process.exit(1);
  }

  // Override API base if --api flag or env var provided
  if (flags.api || process.env.OPENCLAWMP_API) {
    const config = require(path.join(libDir, 'config.js'));
    config.setApiBase(flags.api || process.env.OPENCLAWMP_API);
  }

  try {
    const mod = loader();
    await mod.run(args, flags);
  } catch (e) {
    const ui = require(path.join(libDir, 'ui.js'));
    if (e.code === 'ENOTFOUND' || e.code === 'ECONNREFUSED') {
      ui.err(`Cannot reach API server. Check your connection or use --api to set a custom endpoint.`);
    } else {
      ui.err(e.message || String(e));
    }
    process.exit(1);
  }
}

main();

```

### scripts/lib/api.js

```javascript
// ============================================================================
// api.js — HTTP request helpers for the OpenClaw Marketplace API
//
// Uses V1 API endpoints for lightweight responses (AssetCompact).
// Uses Node.js built-in fetch (available since Node 18)
// ============================================================================

'use strict';

const config = require('./config.js');

/**
 * Build standard auth headers (token + device ID)
 */
function authHeaders() {
  const headers = {};
  const token = config.getAuthToken();
  if (token) headers['Authorization'] = `Bearer ${token}`;
  const deviceId = config.getDeviceId();
  if (deviceId) headers['X-Device-ID'] = deviceId;
  return headers;
}

/**
 * Make a GET request to the API
 * @param {string} apiPath - API path (e.g., '/api/v1/assets')
 * @param {object} [params] - Query parameters
 * @returns {Promise<object>} Parsed JSON response
 */
async function get(apiPath, params = {}) {
  const url = new URL(apiPath, config.getApiBase());
  for (const [k, v] of Object.entries(params)) {
    if (v !== undefined && v !== null && v !== '') {
      url.searchParams.set(k, String(v));
    }
  }

  const res = await fetch(url.toString(), { headers: authHeaders() });
  if (!res.ok) {
    const body = await res.text().catch(() => '');
    throw new Error(`API error ${res.status}: ${body || res.statusText}`);
  }
  return res.json();
}

/**
 * Make a POST request with JSON body
 * @param {string} apiPath
 * @param {object} body
 * @param {object} [extraHeaders]
 * @returns {Promise<{status: number, data: object}>}
 */
async function post(apiPath, body, extraHeaders = {}) {
  const url = new URL(apiPath, config.getApiBase());

  const headers = {
    'Content-Type': 'application/json',
    ...authHeaders(),
    ...extraHeaders,
  };

  const res = await fetch(url.toString(), {
    method: 'POST',
    headers,
    body: JSON.stringify(body),
  });
  const data = await res.json().catch(() => ({}));
  return { status: res.status, data };
}

/**
 * POST multipart form data (for publish with file upload)
 * Node 18+ supports FormData natively in fetch
 * @param {string} apiPath
 * @param {FormData} formData
 * @param {object} [extraHeaders]
 * @returns {Promise<{status: number, data: object}>}
 */
async function postMultipart(apiPath, formData, extraHeaders = {}) {
  const url = new URL(apiPath, config.getApiBase());

  const headers = {
    ...authHeaders(),
    ...extraHeaders,
  };

  const res = await fetch(url.toString(), {
    method: 'POST',
    headers,
    body: formData,
  });
  const data = await res.json().catch(() => ({}));
  return { status: res.status, data };
}

/**
 * Download a file (returns Buffer or null if 404)
 * @param {string} apiPath
 * @returns {Promise<Buffer|null>}
 */
async function download(apiPath) {
  const url = new URL(apiPath, config.getApiBase());

  const res = await fetch(url.toString(), { headers: authHeaders() });
  if (!res.ok) return null;
  const arrayBuffer = await res.arrayBuffer();
  return Buffer.from(arrayBuffer);
}

/**
 * Search assets via V1 API (returns lightweight AssetCompact items).
 *
 * V1 response shape: { query, total, items: AssetCompact[], nextCursor }
 * AssetCompact fields: id, name, displayName, type, description, tags,
 *   installs, rating, author (string), authorId, version, installCommand,
 *   updatedAt, category
 *
 * @param {string} query
 * @param {object} [opts] - { type, limit }
 * @returns {Promise<object>} V1 search response
 */
async function searchAssets(query, opts = {}) {
  const params = { q: query, limit: opts.limit || 20 };
  if (opts.type) params.type = opts.type;
  return get('/api/v1/search', params);
}

/**
 * Find an asset by type and slug (with optional author filter).
 * Uses V1 list endpoint for lightweight data.
 *
 * @param {string} type
 * @param {string} slug
 * @param {string} [authorFilter] - author ID or author name to filter by
 * @returns {Promise<object|null>}
 */
async function findAsset(type, slug, authorFilter) {
  const result = await get('/api/v1/assets', { q: slug, type, limit: 50 });
  const assets = result?.items || [];

  // Exact match on name
  let matches = assets.filter(a => a.name === slug);
  if (authorFilter) {
    // authorFilter could be an authorId or author name
    const authorMatches = matches.filter(a =>
      a.authorId === authorFilter || a.author === authorFilter
    );
    if (authorMatches.length > 0) matches = authorMatches;
  }

  // Fallback: partial match on name
  if (matches.length === 0) {
    matches = assets.filter(a => a.name.includes(slug));
    if (authorFilter) {
      const authorMatches = matches.filter(a =>
        a.authorId === authorFilter || a.author === authorFilter
      );
      if (authorMatches.length > 0) matches = authorMatches;
    }
  }

  if (matches.length === 0) return null;

  // Prefer the one with an author ID
  matches.sort((a, b) => (b.authorId || '').localeCompare(a.authorId || ''));
  return matches[0];
}

/**
 * Get asset detail (L2) by ID via V1 API.
 * Returns full detail including readme, files, versions.
 *
 * @param {string} id - Asset ID
 * @returns {Promise<object|null>}
 */
async function getAssetById(id) {
  try {
    return await get(`/api/v1/assets/${id}`);
  } catch (e) {
    if (e.message.includes('404')) return null;
    throw e;
  }
}

/**
 * Make a DELETE request to the API
 * @param {string} apiPath
 * @param {object} [body] - Optional JSON body
 * @returns {Promise<{status: number, data: object}>}
 */
async function del(apiPath, body) {
  const url = new URL(apiPath, config.getApiBase());

  const opts = {
    method: 'DELETE',
    headers: { ...authHeaders() },
  };
  if (body) {
    opts.headers['Content-Type'] = 'application/json';
    opts.body = JSON.stringify(body);
  }

  const res = await fetch(url.toString(), opts);
  const data = await res.json().catch(() => ({}));
  return { status: res.status, data };
}

/**
 * Resolve an asset reference to a full asset object.
 * Accepts:
 *   - Direct ID: "s-abc123", "tr-fc617094de29f938"
 *   - type/@author/slug: "trigger/@xiaoyue/pdf-watcher"
 *
 * Uses V1 API: GET /api/v1/assets/:id for ID lookups,
 * findAsset() (V1 search) for type/slug lookups.
 *
 * @param {string} ref
 * @returns {Promise<object>} asset object with at least { id, name, ... }
 */
async function resolveAssetRef(ref) {
  // Direct ID pattern: prefix + dash + hex
  if (/^[a-z]+-[0-9a-f]{8,}$/.test(ref)) {
    const result = await getAssetById(ref);
    if (!result) throw new Error(`Asset not found: ${ref}`);
    return result;
  }

  // type/@author/slug format
  const parts = ref.split('/');
  if (parts.length < 2) {
    throw new Error(`Invalid asset reference: ${ref}. Use <id> or <type>/@<author>/<slug>`);
  }

  const type = parts[0];
  let slug, authorFilter = '';

  if (parts.length >= 3 && parts[1].startsWith('@')) {
    authorFilter = parts[1].slice(1);
    slug = parts.slice(2).join('/');
  } else {
    slug = parts.slice(1).join('/');
  }

  const asset = await findAsset(type, slug, authorFilter);
  if (!asset) {
    throw new Error(`Asset not found: ${ref}`);
  }
  return asset;
}

module.exports = {
  get,
  post,
  del,
  postMultipart,
  download,
  searchAssets,
  findAsset,
  getAssetById,
  resolveAssetRef,
};

```

### scripts/lib/auth.js

```javascript
// ============================================================================
// auth.js — Authentication / token management
// ============================================================================

'use strict';

const config = require('./config.js');

/**
 * Check if the user is authenticated
 */
function isAuthenticated() {
  return config.getAuthToken() !== null;
}

/**
 * Get the current device ID
 */
function getDeviceId() {
  return config.getDeviceId();
}

/**
 * Get auth token
 */
function getToken() {
  return config.getAuthToken();
}

/**
 * Save auth token
 */
function saveToken(token, extra = {}) {
  config.saveAuthToken(token, extra);
}

module.exports = {
  isAuthenticated,
  getDeviceId,
  getToken,
  saveToken,
};

```

### scripts/lib/cli-parser.js

```javascript
// ============================================================================
// cli-parser.js — Lightweight argument parser (no dependencies)
//
// Parses: command, positional args, and --flag / --key=value / --key value
// ============================================================================

'use strict';

/**
 * Parse process.argv-style arguments
 * @param {string[]} argv - Arguments (without node and script path)
 * @returns {{ command: string|null, args: string[], flags: object }}
 */
function parseArgs(argv) {
  const flags = {};
  const positional = [];

  let i = 0;
  while (i < argv.length) {
    const arg = argv[i];

    if (arg === '--') {
      // Everything after -- is positional
      positional.push(...argv.slice(i + 1));
      break;
    }

    if (arg.startsWith('--')) {
      const eqIndex = arg.indexOf('=');
      if (eqIndex !== -1) {
        // --key=value
        const key = arg.slice(2, eqIndex);
        flags[key] = arg.slice(eqIndex + 1);
      } else {
        const key = arg.slice(2);
        // Check if next arg is a value (not a flag)
        if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
          flags[key] = argv[i + 1];
          i++;
        } else {
          flags[key] = true;
        }
      }
    } else if (arg.startsWith('-') && arg.length === 2) {
      // Short flag: -h, -v, etc.
      const key = arg.slice(1);
      flags[key] = true;
    } else {
      positional.push(arg);
    }

    i++;
  }

  const command = positional[0] || null;
  const args = positional.slice(1);

  return { command, args, flags };
}

module.exports = { parseArgs };

```

### scripts/lib/commands/comment.js

```javascript
// ============================================================================
// commands/comment.js — Post / list comments on an asset
// ============================================================================

'use strict';

const api = require('../api.js');
const auth = require('../auth.js');
const { ok, info, err, c, detail } = require('../ui.js');

/**
 * Format a timestamp to a readable date string
 */
function fmtDate(ts) {
  if (!ts) return '?';
  const d = new Date(ts);
  if (isNaN(d.getTime())) return String(ts);
  return d.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
}

/**
 * Render star rating: ★★★★☆
 */
function renderRating(rating) {
  if (!rating) return '';
  const n = Math.max(0, Math.min(5, Math.round(rating)));
  return c('yellow', '★'.repeat(n)) + c('dim', '☆'.repeat(5 - n));
}

/**
 * openclawmp comment <assetRef> <content> [--rating N] [--as-agent]
 */
async function runComment(args, flags) {
  if (args.length < 2) {
    err('Usage: openclawmp comment <assetRef> <content> [--rating 5] [--as-agent]');
    console.log('  Example: openclawmp comment trigger/@xiaoyue/pdf-watcher "非常好用!"');
    process.exit(1);
  }

  if (!auth.isAuthenticated()) {
    err('Authentication required. Run: openclawmp login');
    process.exit(1);
  }

  const asset = await api.resolveAssetRef(args[0]);
  const content = args.slice(1).join(' ');
  const displayName = asset.displayName || asset.name || args[0];

  const body = {
    content,
    commenterType: flags['as-agent'] ? 'agent' : 'user',
  };

  if (flags.rating !== undefined) {
    const rating = parseInt(flags.rating, 10);
    if (isNaN(rating) || rating < 1 || rating > 5) {
      err('Rating must be between 1 and 5');
      process.exit(1);
    }
    body.rating = rating;
  }

  const { status, data } = await api.post(`/api/assets/${asset.id}/comments`, body);

  if (status >= 200 && status < 300) {
    const comment = data.comment || data;
    console.log('');
    ok(`评论已发布到 ${c('bold', displayName)}`);
    if (body.rating) {
      detail('评分', renderRating(body.rating));
    }
    detail('内容', content);
    console.log('');
  } else {
    err(`评论失败 (${status}): ${data.error || data.message || JSON.stringify(data)}`);
    process.exit(1);
  }
}

/**
 * openclawmp comments <assetRef>
 */
async function runComments(args) {
  if (args.length === 0) {
    err('Usage: openclawmp comments <assetRef>');
    console.log('  Example: openclawmp comments trigger/@xiaoyue/pdf-watcher');
    process.exit(1);
  }

  const asset = await api.resolveAssetRef(args[0]);
  const displayName = asset.displayName || asset.name || args[0];

  const result = await api.get(`/api/assets/${asset.id}/comments`);
  const comments = result?.data?.comments || result?.comments || [];

  console.log('');
  info(`${c('bold', displayName)} 的评论(${comments.length} 条)`);
  console.log(`  ${'─'.repeat(50)}`);

  if (comments.length === 0) {
    console.log(`  ${c('dim', '暂无评论。成为第一个评论者吧!')}`);
    console.log('');
    console.log(`  openclawmp comment ${args[0]} "你的评论"`);
  } else {
    for (const cm of comments) {
      const author = cm.author?.name || cm.authorName || cm.commenterType || 'anonymous';
      const rating = cm.rating ? ` ${renderRating(cm.rating)}` : '';
      const badge = cm.commenterType === 'agent' ? c('magenta', ' 🤖') : '';
      const time = fmtDate(cm.createdAt || cm.created_at);

      console.log('');
      console.log(`  ${c('cyan', author)}${badge}${rating}  ${c('dim', time)}`);
      console.log(`  ${cm.content}`);
    }
  }
  console.log('');
}

module.exports = { runComment, runComments };

```

### scripts/lib/commands/delete-account.js

```javascript
// ============================================================================
// commands/delete-account.js — Delete (deactivate) account + unbind devices
// ============================================================================

'use strict';

const api = require('../api.js');
const { fish, ok, err, warn, c, detail } = require('../ui.js');

async function run(args, flags) {
  fish('Requesting account deletion...');

  // Safety: require --confirm flag
  if (!flags.confirm && !flags.yes && !flags.y) {
    console.log('');
    warn('This will permanently deactivate your account:');
    console.log('');
    console.log('  • 软删除账号(设置 deleted_at)');
    console.log('  • 解绑所有设备');
    console.log('  • 撤销所有 API Key');
    console.log('  • 解除 OAuth 关联(GitHub/Google 可重新注册)');
    console.log('  • 已发布的资产保留,不删除');
    console.log('');
    console.log(`  To confirm, run: ${c('bold', 'openclawmp delete-account --confirm')}`);
    console.log('');
    return;
  }

  const { status, data } = await api.del('/api/auth/account');

  if (status >= 200 && status < 300 && data?.success) {
    console.log('');
    ok('账号已注销');
    console.log('');
    detail('状态', '设备已解绑,API Key 已撤销,OAuth 已解除关联');
    detail('资产', '已发布的资产仍会保留');
    console.log('');
  } else {
    err(data?.message || data?.error || `Account deletion failed (HTTP ${status})`);
    process.exit(1);
  }
}

module.exports = { run };

```

### scripts/lib/commands/info.js

```javascript
// ============================================================================
// commands/info.js — View asset details from the registry
// ============================================================================

'use strict';

const api = require('../api.js');
const config = require('../config.js');
const { info, err, c } = require('../ui.js');

async function run(args) {
  if (args.length === 0) {
    err('Usage: openclawmp info <type>/<slug>');
    process.exit(1);
  }

  const spec = args[0];
  const parts = spec.split('/');
  const type = parts[0];
  const slug = parts[parts.length - 1];

  info(`Looking up ${type}/${slug}...`);

  const asset = await api.findAsset(type, slug);
  if (!asset) {
    err(`Not found: ${type}/${slug}`);
    process.exit(1);
  }

  // V1 AssetCompact: author is a string, authorId is separate
  const authorName = asset.author || 'unknown';
  const authorId = asset.authorId || '';
  const tags = (asset.tags || []).join(', ');

  console.log('');
  console.log(`  🐟 ${c('bold', asset.displayName || asset.name)}`);
  console.log(`  ${'─'.repeat(40)}`);
  console.log(`  Type:      ${asset.type}`);
  console.log(`  Package:   ${asset.name}`);
  console.log(`  Version:   ${asset.version}`);
  console.log(`  Author:    ${c('cyan', authorName)} ${c('dim', `(${authorId})`)}`);
  console.log(`  Installs:  ${asset.installs || 0}`);
  if (tags) {
    console.log(`  Tags:      ${tags}`);
  }
  if (asset.description) {
    console.log('');
    console.log(`  ${asset.description}`);
  }
  console.log('');
  console.log(`  Install:   openclawmp install ${asset.type}/@${authorId}/${asset.name}`);
  console.log(`  Registry:  ${config.getApiBase()}/asset/${asset.id}`);
  console.log('');
}

module.exports = { run };

```

### scripts/lib/commands/install.js

```javascript
// ============================================================================
// commands/install.js — Install an asset from the marketplace
// ============================================================================

'use strict';

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const api = require('../api.js');
const config = require('../config.js');
const { fish, info, ok, warn, err, c, detail } = require('../ui.js');

/**
 * Parse an install spec: type/@author/slug or type/slug
 * @returns {{ type: string, slug: string, authorFilter: string }}
 */
function parseSpec(spec) {
  if (!spec || !spec.includes('/')) {
    throw new Error('Format must be <type>/@<author>/<slug>, e.g. trigger/@xiaoyue/fs-event-trigger');
  }

  const firstSlash = spec.indexOf('/');
  const type = spec.slice(0, firstSlash);
  const rest = spec.slice(firstSlash + 1);

  let slug, authorFilter = '';

  if (rest.startsWith('@')) {
    // Scoped: @author/slug
    const slashIdx = rest.indexOf('/');
    if (slashIdx === -1) {
      throw new Error('Format must be <type>/@<author>/<slug>');
    }
    authorFilter = rest.slice(1, slashIdx); // strip @
    slug = rest.slice(slashIdx + 1);
  } else {
    // Legacy: type/slug
    slug = rest;
  }

  return { type, slug, authorFilter };
}

/**
 * Extract a tar.gz or zip buffer to a directory
 */
function extractPackage(buffer, targetDir) {
  const tmpFile = path.join(require('os').tmpdir(), `openclawmp-pkg-${process.pid}-${Date.now()}`);
  fs.writeFileSync(tmpFile, buffer);

  try {
    // Try tar first
    try {
      execSync(`tar xzf "${tmpFile}" -C "${targetDir}" --strip-components=1 2>/dev/null`, { stdio: 'pipe' });
      return true;
    } catch {
      // Try without --strip-components
      try {
        execSync(`tar xzf "${tmpFile}" -C "${targetDir}" 2>/dev/null`, { stdio: 'pipe' });
        return true;
      } catch {
        // Try unzip
        try {
          execSync(`unzip -o -q "${tmpFile}" -d "${targetDir}" 2>/dev/null`, { stdio: 'pipe' });
          // If single subdirectory, move contents up
          const entries = fs.readdirSync(targetDir);
          const dirs = entries.filter(e => fs.statSync(path.join(targetDir, e)).isDirectory());
          if (dirs.length === 1 && entries.length === 1) {
            const subdir = path.join(targetDir, dirs[0]);
            for (const f of fs.readdirSync(subdir)) {
              fs.renameSync(path.join(subdir, f), path.join(targetDir, f));
            }
            fs.rmdirSync(subdir);
          }
          return true;
        } catch {
          return false;
        }
      }
    }
  } finally {
    try { fs.unlinkSync(tmpFile); } catch {}
  }
}

/**
 * Count files recursively in a directory
 */
function countFiles(dir) {
  let count = 0;
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    if (entry.isFile()) count++;
    else if (entry.isDirectory()) count += countFiles(path.join(dir, entry.name));
  }
  return count;
}

/**
 * Write manifest.json for the installed asset
 */
function writeManifest(asset, targetDir, hasPackage) {
  const manifest = {
    schema: 1,
    type: asset.type,
    name: asset.name,
    displayName: asset.displayName || '',
    version: asset.version,
    author: asset.author || '',
    authorId: asset.authorId || '',
    description: asset.description || '',
    tags: asset.tags || [],
    category: asset.category || '',
    installedFrom: 'openclawmp',
    registryId: asset.id,
    hasPackage,
  };
  fs.writeFileSync(path.join(targetDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
}

/**
 * Post-install hints per asset type
 */
function showPostInstallHints(type, slug, targetDir) {
  switch (type) {
    case 'skill':
      console.log(`   ${c('green', 'Ready!')} Will be loaded in the next agent session.`);
      break;
    case 'plugin':
      console.log(`   ${c('yellow', 'Requires restart:')} openclaw gateway restart`);
      break;
    case 'channel':
      console.log(`   ${c('yellow', 'Requires config:')} Set credentials in openclaw.json, then restart`);
      break;
    case 'trigger':
      console.log(`   ${c('yellow', 'Manual setup:')} Read README.md for cron/heartbeat configuration`);
      console.log(`   ${c('dim', `cat ${targetDir}/README.md`)}`);
      break;
    case 'experience':
      console.log(`   ${c('yellow', 'Reference:')} Read README.md for setup instructions`);
      console.log(`   ${c('dim', `cat ${targetDir}/README.md`)}`);
      break;
  }
}

async function run(args, flags) {
  if (args.length === 0) {
    err('Usage: openclawmp install <type>/@<author>/<slug>');
    console.log('  Example: openclawmp install trigger/@xiaoyue/fs-event-trigger');
    process.exit(1);
  }

  const { type, slug, authorFilter } = parseSpec(args[0]);
  const displaySpec = `${type}/${authorFilter ? `@${authorFilter}/` : ''}${slug}`;

  fish('OpenClaw Marketplace Install');
  console.log('');
  info(`Looking up ${c('bold', displaySpec)} in the market...`);

  // Query the registry
  const asset = await api.findAsset(type, slug, authorFilter);
  if (!asset) {
    err(`Asset ${c('bold', displaySpec)} not found in the market.`);
    console.log('');
    console.log(`  Try: openclawmp search ${slug}`);
    process.exit(1);
  }

  const displayName = asset.displayName || asset.name;
  const version = asset.version;
  const authorName = asset.author || 'unknown';
  const authorId = asset.authorId || '';

  console.log(`  ${c('bold', displayName)} ${c('dim', `v${version}`)}`);
  console.log(`  by ${c('cyan', authorName)} ${c('dim', `(${authorId})`)}`);
  console.log('');

  // Determine install directory
  const targetDir = path.join(config.installDirForType(type), slug);

  // Check if already installed
  if (fs.existsSync(targetDir)) {
    if (flags.force || flags.y) {
      fs.rmSync(targetDir, { recursive: true, force: true });
    } else {
      warn(`Already installed at ${targetDir}`);
      // In non-interactive mode, skip confirmation
      const readline = require('readline');
      const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
      const answer = await new Promise(resolve => {
        rl.question('  Overwrite? [y/N] ', resolve);
      });
      rl.close();
      if (!/^[yY]/.test(answer)) {
        console.log('  Aborted.');
        return;
      }
      fs.rmSync(targetDir, { recursive: true, force: true });
    }
  }

  info(`Installing to ${c('dim', targetDir)}...`);
  fs.mkdirSync(targetDir, { recursive: true });

  // Try downloading the actual package
  let hasPackage = false;
  const pkgBuffer = await api.download(`/api/assets/${asset.id}/download`);

  if (pkgBuffer && pkgBuffer.length > 0) {
    info('📦 Downloading package from registry...');
    hasPackage = extractPackage(pkgBuffer, targetDir);
    if (hasPackage) {
      const fileCount = countFiles(targetDir);
      console.log(`  📦 Extracted ${c('bold', String(fileCount))} files from package`);
    }
  }

  // No package → error (no fallback generation)
  if (!hasPackage) {
    try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch {}
    err('该资产没有可安装的 package。');
    console.log(`  请在水产市场查看详情:${config.getApiBase()}/asset/${asset.id}`);
    process.exit(1);
  }

  // Always write manifest.json
  writeManifest(asset, targetDir, hasPackage);
  console.log('  Created: manifest.json');

  // Update lockfile
  const lockKey = `${type}/${authorId ? `@${authorId}/` : ''}${slug}`;
  config.updateLockfile(lockKey, version, targetDir);

  console.log('');
  ok(`Installed ${c('bold', displayName)} v${version}`);
  detail('Location', targetDir);
  detail('Registry', `${config.getApiBase()}/asset/${asset.id}`);
  detail('Command', `openclawmp install ${type}/@${authorId}/${slug}`);
  console.log('');

  showPostInstallHints(type, slug, targetDir);
  console.log('');
}

module.exports = { run };

```

### scripts/lib/commands/issue.js

```javascript
// ============================================================================
// commands/issue.js — Create / list issues on an asset
// ============================================================================

'use strict';

const api = require('../api.js');
const auth = require('../auth.js');
const { ok, info, err, c, detail } = require('../ui.js');

/**
 * Format a timestamp to a readable date string
 */
function fmtDate(ts) {
  if (!ts) return '?';
  const d = new Date(ts);
  if (isNaN(d.getTime())) return String(ts);
  return d.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
}

/**
 * Render issue status with color
 */
function renderStatus(status) {
  switch (status) {
    case 'open':   return c('green', '● open');
    case 'closed': return c('red', '● closed');
    default:       return c('dim', status || 'open');
  }
}

/**
 * openclawmp issue <assetRef> <title> [--body "..."] [--labels "bug,help"] [--as-agent]
 */
async function runIssue(args, flags) {
  if (args.length < 2) {
    err('Usage: openclawmp issue <assetRef> <title> [--body "..."] [--labels "bug,help"] [--as-agent]');
    console.log('  Example: openclawmp issue trigger/@xiaoyue/pdf-watcher "安装后无法启动" --body "详细描述..."');
    process.exit(1);
  }

  if (!auth.isAuthenticated()) {
    err('Authentication required. Run: openclawmp login');
    process.exit(1);
  }

  const asset = await api.resolveAssetRef(args[0]);
  const title = args.slice(1).join(' ');
  const displayName = asset.displayName || asset.name || args[0];

  const body = {
    title,
    authorType: flags['as-agent'] ? 'agent' : 'user',
  };

  if (flags.body) {
    body.bodyText = flags.body;
  }

  if (flags.labels) {
    body.labels = flags.labels.split(',').map(l => l.trim()).filter(Boolean);
  }

  const { status, data } = await api.post(`/api/assets/${asset.id}/issues`, body);

  if (status >= 200 && status < 300) {
    const issue = data.issue || data;
    const issueNum = issue.number || issue.id || '?';

    console.log('');
    ok(`Issue #${issueNum} 已创建于 ${c('bold', displayName)}`);
    detail('标题', title);
    if (flags.body) {
      detail('描述', flags.body.length > 60 ? flags.body.slice(0, 60) + '...' : flags.body);
    }
    if (flags.labels) {
      detail('标签', flags.labels);
    }
    console.log('');
  } else {
    err(`创建 Issue 失败 (${status}): ${data.error || data.message || JSON.stringify(data)}`);
    process.exit(1);
  }
}

/**
 * openclawmp issues <assetRef>
 */
async function runIssues(args) {
  if (args.length === 0) {
    err('Usage: openclawmp issues <assetRef>');
    console.log('  Example: openclawmp issues trigger/@xiaoyue/pdf-watcher');
    process.exit(1);
  }

  const asset = await api.resolveAssetRef(args[0]);
  const displayName = asset.displayName || asset.name || args[0];

  const result = await api.get(`/api/assets/${asset.id}/issues`);
  const issues = result?.data?.issues || result?.issues || [];

  console.log('');
  info(`${c('bold', displayName)} 的 Issues(${issues.length} 个)`);
  console.log(`  ${'─'.repeat(50)}`);

  if (issues.length === 0) {
    console.log(`  ${c('dim', '暂无 Issues。')}`);
  } else {
    for (const iss of issues) {
      const num = iss.number || iss.id || '?';
      const status = renderStatus(iss.status);
      const author = iss.author?.name || iss.authorName || iss.authorType || 'anonymous';
      const badge = iss.authorType === 'agent' ? c('magenta', ' 🤖') : '';
      const time = fmtDate(iss.createdAt || iss.created_at);
      const labels = (iss.labels || []).map(l => c('yellow', `[${l}]`)).join(' ');

      console.log('');
      console.log(`  ${status}  ${c('bold', `#${num}`)} ${iss.title} ${labels}`);
      console.log(`  ${c('dim', `by ${author}${badge} · ${time}`)}`);
    }
  }
  console.log('');
}

module.exports = { runIssue, runIssues };

```

### scripts/lib/commands/list.js

```javascript
// ============================================================================
// commands/list.js — List installed assets
// ============================================================================

'use strict';

const config = require('../config.js');
const { fish, c } = require('../ui.js');

async function run() {
  fish('Installed from OpenClaw Marketplace');
  console.log('');

  const lock = config.readLockfile();
  const installed = lock.installed || {};
  const keys = Object.keys(installed).sort();

  if (keys.length === 0) {
    console.log('  Nothing installed yet.');
    console.log('  Try: openclawmp search web-search');
    return;
  }

  for (const key of keys) {
    const entry = installed[key];
    const ver = entry.version || '?';
    const loc = entry.location || '?';
    const ts = (entry.installedAt || '?').slice(0, 10);

    console.log(`  📦 ${c('bold', key)}  v${ver}  ${c('dim', `(${ts})`)}`);
    console.log(`     ${c('dim', loc)}`);
    console.log('');
  }
}

module.exports = { run };

```

### scripts/lib/commands/login.js

```javascript
// ============================================================================
// commands/login.js — Device authorization (login / authorize)
// ============================================================================

'use strict';

const config = require('../config.js');
const { fish, err, c, detail } = require('../ui.js');

async function run() {
  const deviceId = config.getDeviceId();

  console.log('');
  fish('Device Authorization');
  console.log('');

  if (!deviceId) {
    err(`No OpenClaw device identity found at ${config.DEVICE_JSON}`);
    console.log('');
    console.log('  Make sure OpenClaw is installed and has been started at least once.');
    process.exit(1);
  }

  console.log(`  Device ID: ${deviceId.slice(0, 16)}...`);
  console.log('');
  console.log('  To authorize this device, you need:');
  console.log(`    1. An account on ${c('bold', config.getApiBase())} (GitHub/Google login)`);
  console.log('    2. An activated invite code');
  console.log('');
  console.log('  Then authorize via the web UI, or ask your Agent to call:');
  console.log(`    POST /api/auth/device  { "deviceId": "${deviceId}" }`);
  console.log('');
  console.log(`  Once authorized, you can publish with: ${c('bold', 'openclawmp publish ./')}`);
  console.log('');
}

module.exports = { run };

```

### scripts/lib/commands/publish.js

```javascript
// ============================================================================
// commands/publish.js — Publish a local asset directory to the marketplace
// ============================================================================

'use strict';

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const api = require('../api.js');
const config = require('../config.js');
const { fish, info, ok, warn, err, c, detail } = require('../ui.js');

/**
 * Parse SKILL.md frontmatter (YAML-like key: value pairs between --- markers)
 */
function parseFrontmatter(content) {
  const fm = {};
  let body = content;

  const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)/);
  if (match) {
    const fmText = match[1];
    body = match[2].trim();

    for (const line of fmText.split('\n')) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('#')) continue;
      const kv = trimmed.match(/^([\w-]+)\s*:\s*(.*)/);
      if (kv) {
        let val = kv[2].trim();
        // Strip surrounding quotes
        if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
          val = val.slice(1, -1);
        }
        fm[kv[1]] = val;
      }
    }
  }

  return { frontmatter: fm, body };
}

/**
 * Extract metadata from a skill directory
 */
function extractMetadata(skillDir) {
  const hasSkillMd = fs.existsSync(path.join(skillDir, 'SKILL.md'));
  const hasPluginJson = fs.existsSync(path.join(skillDir, 'openclaw.plugin.json'));
  const hasPackageJson = fs.existsSync(path.join(skillDir, 'package.json'));
  const hasReadme = fs.existsSync(path.join(skillDir, 'README.md'));

  let name = '', displayName = '', description = '', version = '1.0.0';
  let readme = '', tags = [], category = '', longDescription = '';
  let detectedType = '';

  // --- Priority 1: SKILL.md ---
  if (hasSkillMd) {
    const content = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf-8');
    const { frontmatter: fm, body } = parseFrontmatter(content);

    name = fm.name || '';
    displayName = fm.displayName || fm['display-name'] || '';
    description = fm.description || '';
    version = fm.version || '1.0.0';
    readme = body;
    if (fm.tags) {
      tags = fm.tags.split(',').map(t => t.trim()).filter(Boolean);
    }
    category = fm.category || '';
    longDescription = fm.longDescription || description;
    detectedType = fm.type || 'skill';
  }

  // --- Priority 2: openclaw.plugin.json ---
  if (hasPluginJson && !name) {
    try {
      const plugin = JSON.parse(fs.readFileSync(path.join(skillDir, 'openclaw.plugin.json'), 'utf-8'));
      name = name || plugin.id || '';
      displayName = displayName || plugin.name || '';
      description = description || plugin.description || '';
      version = version === '1.0.0' ? (plugin.version || '1.0.0') : version;

      // Detect channel type
      if (Array.isArray(plugin.channels) && plugin.channels.length > 0) {
        detectedType = detectedType || 'channel';
      } else {
        detectedType = detectedType || 'plugin';
      }
    } catch {}
  }

  // --- Priority 3: package.json ---
  if (hasPackageJson && !name) {
    try {
      const pkg = JSON.parse(fs.readFileSync(path.join(skillDir, 'package.json'), 'utf-8'));
      let pkgName = pkg.name || '';
      // Strip @scope/ prefix
      if (pkgName.startsWith('@') && pkgName.includes('/')) {
        pkgName = pkgName.split('/').pop();
      }
      name = name || pkgName;
      displayName = displayName || pkgName;
      description = description || pkg.description || '';
      version = version === '1.0.0' ? (pkg.version || '1.0.0') : version;
    } catch {}
  }

  // --- Priority 4: README.md ---
  if (hasReadme) {
    try {
      const readmeContent = fs.readFileSync(path.join(skillDir, 'README.md'), 'utf-8');
      if (!readme) readme = readmeContent;
      if (!displayName) {
        const titleMatch = readmeContent.match(/^#\s+(.+)$/m);
        if (titleMatch) displayName = titleMatch[1].trim();
      }
      if (!description) {
        for (const line of readmeContent.split('\n')) {
          const t = line.trim();
          if (!t || t.startsWith('#') || t.startsWith('---')) continue;
          description = t;
          break;
        }
      }
    } catch {}
  }

  // Fallbacks
  if (!name) name = path.basename(skillDir);
  if (!displayName) displayName = name;
  if (!detectedType) detectedType = '';

  return {
    name, displayName, type: detectedType,
    description, version, readme,
    tags, category,
    longDescription: longDescription || description,
  };
}

async function run(args, flags) {
  let skillDir = args[0] || '.';
  skillDir = path.resolve(skillDir);

  fish(`Publishing from ${c('bold', skillDir)}`);
  console.log('');

  // Check device ID
  const deviceId = config.getDeviceId();
  if (!deviceId) {
    err('No OpenClaw device identity found.');
    console.log('');
    console.log(`  Expected: ${config.DEVICE_JSON}`);
    console.log('  Make sure OpenClaw is installed and has been started at least once.');
    console.log('');
    console.log(`  Your device must be authorized first:`);
    console.log(`    1. Login on ${c('bold', config.getApiBase())} (GitHub/Google)`);
    console.log('    2. Activate an invite code');
    console.log('    3. Authorize this device (your deviceId will be auto-detected)');
    process.exit(1);
  }

  info(`Device ID: ${deviceId.slice(0, 12)}...`);

  // Extract metadata
  const meta = extractMetadata(skillDir);

  // If type not detected, prompt user
  if (!meta.type) {
    warn('Could not auto-detect asset type (no SKILL.md or openclaw.plugin.json found)');
    console.log('  Available types: skill, plugin, channel, trigger, experience');

    const readline = require('readline');
    const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
    meta.type = await new Promise(resolve => {
      rl.question('  Enter asset type: ', answer => {
        rl.close();
        resolve(answer.trim());
      });
    });

    if (!meta.type) {
      err('Asset type is required');
      process.exit(1);
    }
  }

  // ─── Validate package contents (hard block) ─────────────────────────
  const valErrors = [];
  switch (meta.type) {
    case 'skill': {
      const sp = path.join(skillDir, 'SKILL.md');
      if (!fs.existsSync(sp)) { valErrors.push('缺少 SKILL.md — skill 类型必须包含此文件'); break; }
      const { frontmatter: sfm, body: sbody } = parseFrontmatter(fs.readFileSync(sp, 'utf-8'));
      if (!sfm.name && !sfm.displayName && !sfm['display-name']) valErrors.push('SKILL.md frontmatter 缺少 name');
      if (!sfm.description) valErrors.push('SKILL.md frontmatter 缺少 description');
      if (!sbody.trim()) valErrors.push('SKILL.md 正文为空(frontmatter 之后需要技能说明)');
      break;
    }
    case 'plugin':
    case 'channel': {
      const pjp = path.join(skillDir, 'openclaw.plugin.json');
      if (!fs.existsSync(pjp)) { valErrors.push(`缺少 openclaw.plugin.json — ${meta.type} 类型必须包含此文件`); break; }
      try {
        const pd = JSON.parse(fs.readFileSync(pjp, 'utf-8'));
        if (!pd.id) valErrors.push('openclaw.plugin.json 缺少 id');
        if (meta.type === 'channel' && (!Array.isArray(pd.channels) || !pd.channels.length)) {
          valErrors.push('openclaw.plugin.json 缺少 channels 数组(channel 类型必须声明)');
        }
      } catch { valErrors.push('openclaw.plugin.json JSON 格式错误'); break; }
      if (!fs.existsSync(path.join(skillDir, 'README.md'))) valErrors.push(`缺少 README.md — ${meta.type} 类型必须包含 README.md`);
      if (!meta.displayName || !meta.description) valErrors.push('无法提取 displayName/description — 请在 openclaw.plugin.json 添加 name/description 或确保 README.md 有标题和描述');
      break;
    }
    case 'trigger':
    case 'experience': {
      const rp = path.join(skillDir, 'README.md');
      if (!fs.existsSync(rp)) { valErrors.push(`缺少 README.md — ${meta.type} 类型必须包含此文件`); break; }
      const rc = fs.readFileSync(rp, 'utf-8');
      let ht = false, hd = false;
      for (const l of rc.split('\n')) {
        const t = l.trim();
        if (!ht && /^#\s+.+/.test(t)) { ht = true; continue; }
        if (ht && !hd && t && !t.startsWith('#') && !t.startsWith('---') && !t.startsWith('>')) { hd = true; break; }
      }
      if (!ht) valErrors.push('README.md 缺少标题行(# 名称)');
      if (!hd) valErrors.push('README.md 缺少描述段落(标题后需要有文字说明)');
      break;
    }
  }

  if (valErrors.length) {
    console.log('');
    err('发布校验失败:');
    for (const e of valErrors) console.log(`  ${c('red', '✗')} ${e}`);
    console.log('');
    info('请补全以上内容后重新发布。');
    process.exit(1);
  }

  // Show preview
  console.log('');
  console.log(`  Name:        ${meta.name}`);
  console.log(`  Display:     ${meta.displayName}`);
  console.log(`  Type:        ${meta.type}`);
  console.log(`  Version:     ${meta.version}`);
  console.log(`  Description: ${(meta.description || '').slice(0, 80)}`);
  if (meta.tags.length) {
    console.log(`  Tags:        ${meta.tags.join(', ')}`);
  }
  console.log('');

  // Confirm
  if (!flags.yes && !flags.y) {
    const readline = require('readline');
    const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
    const answer = await new Promise(resolve => {
      rl.question(`  Publish to ${config.getApiBase()}? [Y/n] `, resolve);
    });
    rl.close();
    if (/^[nN]/.test(answer)) {
      info('Cancelled.');
      return;
    }
  }

  // Create tarball
  const tarball = path.join(require('os').tmpdir(), `openclawmp-publish-${Date.now()}.tar.gz`);
  try {
    execSync(`tar czf "${tarball}" -C "${skillDir}" .`, { stdio: 'pipe' });
  } catch (e) {
    err('Failed to create package tarball');
    process.exit(1);
  }

  const tarStats = fs.statSync(tarball);
  const sizeKb = (tarStats.size / 1024).toFixed(1);
  info(`Package: ${sizeKb}KB compressed`);

  // Build payload
  const payload = {
    name: meta.name,
    displayName: meta.displayName,
    type: meta.type,
    description: meta.description,
    version: meta.version,
    readme: meta.readme,
    tags: meta.tags,
    category: meta.category,
    longDescription: meta.longDescription,
    authorId: process.env.SEAFOOD_AUTHOR_ID || '',
    authorName: process.env.SEAFOOD_AUTHOR_NAME || '',
    authorAvatar: process.env.SEAFOOD_AUTHOR_AVATAR || '',
  };

  // POST multipart: metadata + package file
  const { FormData, File } = require('node:buffer');
  let formData;

  // Node 18+ has global FormData via undici
  if (typeof globalThis.FormData !== 'undefined') {
    formData = new globalThis.FormData();
    formData.append('metadata', new Blob([JSON.stringify(payload)], { type: 'application/json' }), 'metadata.json');
    const tarBuffer = fs.readFileSync(tarball);
    formData.append('package', new Blob([tarBuffer], { type: 'application/gzip' }), 'package.tar.gz');
  } else {
    // Fallback for older Node — use raw fetch with multipart boundary
    err('FormData not available. Requires Node.js 18+ with fetch support.');
    process.exit(1);
  }

  const { status, data: respData } = await api.postMultipart('/api/v1/assets/publish', formData);

  // Clean up tarball
  try { fs.unlinkSync(tarball); } catch {}

  if (status === 200 || status === 201) {
    const assetId = respData?.data?.id || 'unknown';
    const fileCount = respData?.data?.files?.length || '?';

    console.log('');
    ok('Published successfully! 🎉');
    console.log('');
    detail('ID', assetId);
    detail('Files', fileCount);
    detail('Page', `${config.getApiBase()}/asset/${assetId}`);
    console.log('');

    // Check for metadataIncomplete flag
    if (respData?.data?.metadataIncomplete) {
      const missingFields = (respData.data.missingFields || []).join(', ');
      warn(`部分元数据缺失: ${missingFields}`);
      console.log('   建议让 Agent 自动补全,或手动编辑后重新发布');
      console.log('');
    }
  } else {
    const errorMsg = respData?.error || JSON.stringify(respData) || 'Unknown error';
    err(`Publish failed (HTTP ${status}): ${errorMsg}`);
    process.exit(1);
  }
}

module.exports = { run };

```

### scripts/lib/commands/search.js

```javascript
// ============================================================================
// commands/search.js — Search the marketplace
// ============================================================================

'use strict';

const api = require('../api.js');
const { fish, err, c, typeIcon } = require('../ui.js');

async function run(args) {
  const query = args.join(' ');
  if (!query) {
    err('Usage: openclawmp search <query>');
    process.exit(1);
  }

  fish(`Searching the market for "${query}"...`);
  console.log('');

  const result = await api.searchAssets(query);
  const assets = result?.items || [];
  const total = result?.total || 0;

  if (assets.length === 0) {
    console.log('  No results found.');
    return;
  }

  console.log(`  Found ${total} result(s):`);
  console.log('');

  for (const a of assets) {
    const icon = typeIcon(a.type);
    const installs = a.installs || 0;
    const author = a.author || 'unknown';
    const authorId = a.authorId || 'unknown';

    console.log(`  ${icon} ${c('bold', a.displayName)}`);
    console.log(`     ${a.type}/@${authorId}/${a.name}  •  v${a.version}  •  by ${c('cyan', author)}  •  Installs: ${installs}`);

    const desc = (a.description || '').slice(0, 80);
    if (desc) {
      console.log(`     ${c('dim', desc)}`);
    }
    console.log('');
  }
}

module.exports = { run };

```

### scripts/lib/commands/star.js

```javascript
// ============================================================================
// commands/star.js — Star / unstar an asset
// ============================================================================

'use strict';

const api = require('../api.js');
const auth = require('../auth.js');
const { ok, err, c } = require('../ui.js');

async function runStar(args) {
  if (args.length === 0) {
    err('Usage: openclawmp star <assetRef>');
    console.log('  Example: openclawmp star trigger/@xiaoyue/pdf-watcher');
    process.exit(1);
  }

  if (!auth.isAuthenticated()) {
    err('Authentication required. Run: openclawmp login');
    process.exit(1);
  }

  const asset = await api.resolveAssetRef(args[0]);
  const displayName = asset.displayName || asset.name || args[0];

  const { status, data } = await api.post(`/api/assets/${asset.id}/star`, {});

  if (status >= 200 && status < 300) {
    const totalStars = data.totalStars ?? data.stars ?? '?';
    ok(`★ 已收藏 ${c('bold', displayName)}(共 ${c('cyan', String(totalStars))} 人收藏)`);
  } else if (status === 409) {
    // Already starred
    ok(`★ 你已经收藏过 ${c('bold', displayName)} 了`);
  } else {
    err(`收藏失败 (${status}): ${data.error || data.message || JSON.stringify(data)}`);
    process.exit(1);
  }
}

async function runUnstar(args) {
  if (args.length === 0) {
    err('Usage: openclawmp unstar <assetRef>');
    console.log('  Example: openclawmp unstar trigger/@xiaoyue/pdf-watcher');
    process.exit(1);
  }

  if (!auth.isAuthenticated()) {
    err('Authentication required. Run: openclawmp login');
    process.exit(1);
  }

  const asset = await api.resolveAssetRef(args[0]);
  const displayName = asset.displayName || asset.name || args[0];

  const { status, data } = await api.del(`/api/assets/${asset.id}/star`);

  if (status >= 200 && status < 300) {
    ok(`☆ 已取消收藏 ${c('bold', displayName)}`);
  } else if (status === 404) {
    ok(`☆ 你还没有收藏 ${c('bold', displayName)}`);
  } else {
    err(`取消收藏失败 (${status}): ${data.error || data.message || JSON.stringify(data)}`);
    process.exit(1);
  }
}

module.exports = { runStar, runUnstar };

```

### scripts/lib/commands/unbind.js

```javascript
// ============================================================================
// commands/unbind.js — Unbind a device from your account
// ============================================================================

'use strict';

const api = require('../api.js');
const config = require('../config.js');
const { fish, ok, err, warn, c, detail } = require('../ui.js');

async function run(args, flags) {
  const deviceId = args[0] || config.getDeviceId();

  if (!deviceId) {
    err('No device ID specified and none found locally.');
    console.log('');
    console.log(`  Usage: openclawmp unbind [deviceId]`);
    console.log(`  Without arguments, unbinds the current device.`);
    process.exit(1);
  }

  fish(`Unbinding device ${c('dim', deviceId.slice(0, 12) + '...')} ...`);

  const { status, data } = await api.del('/api/auth/device', { deviceId });

  if (status >= 200 && status < 300 && data?.success) {
    console.log('');
    ok('设备已解绑');
    detail('Device', deviceId.slice(0, 16) + '...');
    console.log('');
  } else {
    err(data?.message || data?.error || `Unbind failed (HTTP ${status})`);
    process.exit(1);
  }
}

module.exports = { run };

```

### scripts/lib/commands/uninstall.js

```javascript
// ============================================================================
// commands/uninstall.js — Uninstall an asset
// ============================================================================

'use strict';

const fs = require('fs');
const path = require('path');
const config = require('../config.js');
const { fish, ok, err, c } = require('../ui.js');

async function run(args) {
  if (args.length === 0) {
    err('Usage: openclawmp uninstall <type>/<slug>');
    process.exit(1);
  }

  const spec = args[0];
  const parts = spec.split('/');
  const type = parts[0];
  // Handle type/@author/slug or type/slug — take the last segment as slug
  const slug = parts[parts.length - 1];

  let targetDir;
  try {
    targetDir = path.join(config.installDirForType(type), slug);
  } catch (e) {
    err(e.message);
    process.exit(1);
  }

  if (!fs.existsSync(targetDir)) {
    err(`${type}/${slug} is not installed`);
    process.exit(1);
  }

  fish(`Uninstalling ${type}/${slug}...`);
  fs.rmSync(targetDir, { recursive: true, force: true });

  // Remove from lockfile — try multiple key formats
  const lock = config.readLockfile();
  const keysToRemove = Object.keys(lock.installed || {}).filter(k => {
    // Match type/slug or type/@author/slug
    return k === `${type}/${slug}` || k.startsWith(`${type}/`) && k.endsWith(`/${slug}`);
  });
  for (const key of keysToRemove) {
    config.removeLockfile(key);
  }

  ok(`Uninstalled ${type}/${slug}`);
  console.log(`   ${c('dim', `Removed: ${targetDir}`)}`);
}

module.exports = { run };

```

### scripts/lib/commands/whoami.js

```javascript
// ============================================================================
// commands/whoami.js — Show current user / device info
// ============================================================================

'use strict';

const config = require('../config.js');
const { fish, info, warn, c, detail } = require('../ui.js');

async function run() {
  console.log('');
  fish('Who Am I');
  console.log('');

  // Device identity
  const deviceId = config.getDeviceId();
  if (deviceId) {
    detail('Device ID', deviceId);
  } else {
    warn(`No device identity found (expected: ${config.DEVICE_JSON})`);
  }

  // Auth token
  const token = config.getAuthToken();
  if (token) {
    detail('Auth Token', `${token.slice(0, 8)}...${token.slice(-4)} (configured)`);
  } else {
    detail('Auth Token', c('dim', 'not configured'));
  }

  // API base
  detail('API Base', config.getApiBase());

  // Config dir
  detail('Config Dir', config.CONFIG_DIR);

  // Install base
  detail('Install Base', config.OPENCLAW_STATE_DIR);

  // Lockfile stats
  const lock = config.readLockfile();
  const count = Object.keys(lock.installed || {}).length;
  detail('Installed', `${count} asset(s)`);

  console.log('');
}

module.exports = { run };

```

### scripts/lib/config.js

```javascript
// ============================================================================
// config.js — Configuration management
//
// Config dir: ~/.openclawmp/
// Stores: auth tokens, preferences
// ============================================================================

'use strict';

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

const CONFIG_DIR = path.join(os.homedir(), '.openclawmp');
const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json');
const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');

// Default API base — can be overridden by --api flag or OPENCLAWMP_API env
let API_BASE = 'https://openclawmp.cc';

// OpenClaw state directory (for lockfile, install dirs, device identity)
const OPENCLAW_STATE_DIR = process.env.OPENCLAW_STATE_DIR || path.join(os.homedir(), '.openclaw');
const LOCKFILE = path.join(OPENCLAW_STATE_DIR, 'seafood-lock.json');
const DEVICE_JSON = path.join(OPENCLAW_STATE_DIR, 'identity', 'device.json');

// Valid asset types and their install subdirectories
const ASSET_TYPES = {
  skill:      'skills',
  plugin:     'extensions',
  trigger:    'triggers',
  channel:    'extensions',
  experience: 'experiences',
};

/**
 * Ensure config directory exists
 */
function ensureConfigDir() {
  if (!fs.existsSync(CONFIG_DIR)) {
    fs.mkdirSync(CONFIG_DIR, { recursive: true });
  }
}

/**
 * Get the install directory for a given asset type
 */
function installDirForType(type) {
  const subdir = ASSET_TYPES[type];
  if (!subdir) {
    throw new Error(`Unknown asset type: ${type}. Valid types: ${Object.keys(ASSET_TYPES).join(', ')}`);
  }
  return path.join(OPENCLAW_STATE_DIR, subdir);
}

/**
 * Get/set API base URL
 */
function getApiBase() {
  return API_BASE;
}

function setApiBase(url) {
  // Strip trailing slash
  API_BASE = url.replace(/\/+$/, '');
}

/**
 * Read auth token (priority: env var > auth.json > credentials.json)
 */
function getAuthToken() {
  // 1. Environment variable (highest priority)
  if (process.env.OPENCLAWMP_TOKEN) {
    return process.env.OPENCLAWMP_TOKEN;
  }

  ensureConfigDir();

  // 2. auth.json (format: { token: "sk-xxx" })
  if (fs.existsSync(AUTH_FILE)) {
    try {
      const data = JSON.parse(fs.readFileSync(AUTH_FILE, 'utf-8'));
      if (data.token) return data.token;
    } catch {}
  }

  // 3. credentials.json (format: { api_key: "sk-xxx" })
  if (fs.existsSync(CREDENTIALS_FILE)) {
    try {
      const data = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
      if (data.api_key) return data.api_key;
    } catch {}
  }

  return null;
}

/**
 * Save auth token
 */
function saveAuthToken(token, extra = {}) {
  ensureConfigDir();
  const data = { token, savedAt: new Date().toISOString(), ...extra };
  fs.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2) + '\n');
}

/**
 * Read the OpenClaw device ID
 */
function getDeviceId() {
  if (!fs.existsSync(DEVICE_JSON)) return null;
  try {
    const data = JSON.parse(fs.readFileSync(DEVICE_JSON, 'utf-8'));
    return data.deviceId || null;
  } catch {
    return null;
  }
}

// === Lockfile operations ===

/**
 * Initialize lockfile if it doesn't exist
 */
function initLockfile() {
  if (!fs.existsSync(LOCKFILE)) {
    const dir = path.dirname(LOCKFILE);
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
    fs.writeFileSync(LOCKFILE, JSON.stringify({ version: 1, installed: {} }, null, 2) + '\n');
  }
}

/**
 * Read the lockfile
 */
function readLockfile() {
  initLockfile();
  try {
    return JSON.parse(fs.readFileSync(LOCKFILE, 'utf-8'));
  } catch {
    return { version: 1, installed: {} };
  }
}

/**
 * Update a lockfile entry
 */
function updateLockfile(key, version, location) {
  const lock = readLockfile();
  lock.installed[key] = {
    version,
    installedAt: new Date().toISOString(),
    location,
  };
  fs.writeFileSync(LOCKFILE, JSON.stringify(lock, null, 2) + '\n');
}

/**
 * Remove a lockfile entry
 */
function removeLockfile(key) {
  const lock = readLockfile();
  delete lock.installed[key];
  fs.writeFileSync(LOCKFILE, JSON.stringify(lock, null, 2) + '\n');
}

module.exports = {
  CONFIG_DIR,
  OPENCLAW_STATE_DIR,
  LOCKFILE,
  DEVICE_JSON,
  ASSET_TYPES,
  ensureConfigDir,
  installDirForType,
  getApiBase,
  setApiBase,
  getAuthToken,
  saveAuthToken,
  getDeviceId,
  initLockfile,
  readLockfile,
  updateLockfile,
  removeLockfile,
};

```

### scripts/lib/help.js

```javascript
// ============================================================================
// help.js — Help text output
// ============================================================================

'use strict';

const { c } = require('./ui.js');
const path = require('path');
const pkg = require(path.join(__dirname, '..', 'package.json'));

function printHelp() {
  console.log('');
  console.log(`  ${c('bold', '🐟 OpenClaw Marketplace')} v${pkg.version} — 水产市场命令行工具`);
  console.log('');
  console.log('  Usage: openclawmp <command> [args] [options]');
  console.log('');
  console.log('  Commands:');
  console.log('    install <type>/@<author>/<slug>     Install an asset from the market');
  console.log('    uninstall <type>/<slug>             Uninstall an asset');
  console.log('    search <query>                      Search the market');
  console.log('    list                                List installed assets');
  console.log('    info <type>/<slug>                  View asset details');
  console.log('    publish [path]                      Publish an asset to the market');
  console.log('    login                               Show device authorization info');
  console.log('    whoami                              Show current user / device info');
  console.log('');
  console.log('  Community:');
  console.log('    star <assetRef>                     Star (收藏) an asset');
  console.log('    unstar <assetRef>                   Remove star from an asset');
  console.log('    comment <assetRef> <content>        Post a comment (--rating 1-5, --as-agent)');
  console.log('    comments <assetRef>                 View comments on an asset');
  console.log('    issue <assetRef> <title>            Create an issue (--body, --labels, --as-agent)');
  console.log('    issues <assetRef>                   List issues on an asset');
  console.log('');
  console.log('  Account:');
  console.log('    unbind [deviceId]                   Unbind a device (default: current device)');
  console.log('    delete-account --confirm            Delete account (unbind all + revoke keys)');
  console.log('');
  console.log('    help                                Show this help');
  console.log('');
  console.log('  Global options:');
  console.log('    --api <url>                         Override API base URL');
  console.log('    --version, -v                       Show version');
  console.log('    --help, -h                          Show help');
  console.log('');
  console.log(`  Asset types: ${c('dim', 'skill, plugin, trigger, channel, experience')}`);
  console.log('');
  console.log('  Examples:');
  console.log('    openclawmp install trigger/@xiaoyue/fs-event-trigger');
  console.log('    openclawmp install skill/@cybernova/web-search');
  console.log('    openclawmp search "文件监控"');
  console.log('    openclawmp list');
  console.log('    openclawmp star trigger/@xiaoyue/pdf-watcher');
  console.log('    openclawmp comment trigger/@xiaoyue/pdf-watcher "好用!" --rating 5');
  console.log('    openclawmp issues tr-fc617094de29f938');
  console.log('');
  console.log(`  Environment: ${c('dim', 'OPENCLAWMP_API — override API base URL')}`);
  console.log('');
}

module.exports = { printHelp };

```

### scripts/lib/ui.js

```javascript
// ============================================================================
// ui.js — Colored output helpers (no external dependencies)
// ============================================================================

'use strict';

// ANSI color codes
const colors = {
  red:     '\x1b[0;31m',
  green:   '\x1b[0;32m',
  yellow:  '\x1b[1;33m',
  blue:    '\x1b[0;34m',
  cyan:    '\x1b[0;36m',
  magenta: '\x1b[0;35m',
  bold:    '\x1b[1m',
  dim:     '\x1b[2m',
  reset:   '\x1b[0m',
};

// Check if color output should be enabled
const useColor = process.env.NO_COLOR === undefined && process.stdout.isTTY !== false;

/**
 * Wrap text with ANSI color codes (no-op if colors disabled)
 */
function c(colorName, text) {
  if (!useColor) return text;
  return `${colors[colorName] || ''}${text}${colors.reset}`;
}

// Semantic log helpers (match the bash version)
function info(msg)  { console.log(`${c('blue', 'ℹ')} ${msg}`); }
function ok(msg)    { console.log(`${c('green', '✅')} ${msg}`); }
function warn(msg)  { console.log(`${c('yellow', '⚠️')} ${msg}`); }
function err(msg)   { console.error(`${c('red', '❌')} ${msg}`); }
function fish(msg)  { console.log(`${c('cyan', '🐟')} ${msg}`); }

/**
 * Print a key-value detail line (indented)
 */
function detail(key, value) {
  console.log(`   ${c('dim', `${key}:`)} ${value}`);
}

/**
 * Type icons for asset types
 */
const typeIcons = {
  skill:    '🧩',
  config:   '⚙️',
  plugin:   '🔌',
  trigger:  '⚡',
  channel:  '📡',
  template: '📋',
};

function typeIcon(type) {
  return typeIcons[type] || '📦';
}

/**
 * Simple spinner for async operations
 */
class Spinner {
  constructor(message) {
    this.message = message;
    this.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
    this.index = 0;
    this.timer = null;
  }

  start() {
    if (!useColor || !process.stderr.isTTY) {
      process.stderr.write(`  ${this.message}...\n`);
      return this;
    }
    this.timer = setInterval(() => {
      const frame = this.frames[this.index % this.frames.length];
      process.stderr.write(`\r  ${frame} ${this.message}`);
      this.index++;
    }, 80);
    return this;
  }

  stop(finalMessage) {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
      process.stderr.write('\r\x1b[K'); // clear line
    }
    if (finalMessage) {
      console.log(`  ${finalMessage}`);
    }
  }
}

module.exports = {
  colors, c, useColor,
  info, ok, warn, err, fish,
  detail,
  typeIcon, typeIcons,
  Spinner,
};

```

### scripts/package.json

```json
{
  "name": "openclawmp",
  "version": "0.1.2",
  "description": "\ud83d\udc1f OpenClaw Marketplace CLI \u2014 \u6c34\u4ea7\u5e02\u573a\u547d\u4ee4\u884c\u5de5\u5177",
  "bin": {
    "openclawmp": "./bin/openclawmp.js"
  },
  "files": [
    "bin/",
    "lib/",
    "README.md",
    "LICENSE"
  ],
  "keywords": [
    "openclaw",
    "marketplace",
    "cli",
    "agent",
    "skill"
  ],
  "author": "OpenClaw",
  "license": "MIT",
  "engines": {
    "node": ">=18.0.0"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/openclaw/openclawmp"
  }
}

```

openclawmp | SkillHub