feat: 添加 claw-market CLI 工具并更新 skill 使用 CLI
- 创建 pnpm monorepo 结构 (pnpm-workspace.yaml) - 添加 @ricardweii/claw-market CLI 包 - register/heartbeat/task/config 命令 - 中英文国际化支持 - JSON 输出格式支持 - 更新 openclaw-reporter skill 使用 CLI 替代 curl - 修复注册 API 返回缺少 name 字段的问题 - 更新 CLAUDE.md 文档说明 monorepo 结构
This commit is contained in:
69
CLAUDE.md
69
CLAUDE.md
@@ -4,10 +4,33 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
OpenClaw Market is a real-time global heatmap dashboard that visualizes AI agent ("claw") activity worldwide. Agents install the `openclaw-reporter` skill, which sends anonymous heartbeats and task summaries to this server. The frontend renders a 3D globe and dashboard panels showing live activity via SSE.
|
OpenClaw Market is a real-time global heatmap dashboard that visualizes AI agent ("claw") activity worldwide. Agents install the `openclaw-reporter` skill or `@ricardweii/claw-market` CLI, which sends anonymous heartbeats and task summaries to this server. The frontend renders a 3D globe and dashboard panels showing live activity via SSE.
|
||||||
|
|
||||||
|
## Monorepo Structure
|
||||||
|
|
||||||
|
This is a pnpm monorepo with two packages:
|
||||||
|
|
||||||
|
```
|
||||||
|
openclaw-market/
|
||||||
|
├── app/ # Next.js application (main web app)
|
||||||
|
├── lib/ # Shared libraries for web app
|
||||||
|
├── packages/
|
||||||
|
│ └── claw-market/ # CLI tool (published to npm)
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── index.ts # CLI entry point
|
||||||
|
│ │ ├── commands/ # register, heartbeat, task, config
|
||||||
|
│ │ ├── lib/ # api, config, validate, platform
|
||||||
|
│ │ └── i18n/ # en.json, zh.json
|
||||||
|
│ └── package.json
|
||||||
|
├── skill/
|
||||||
|
│ └── openclaw-reporter/ # Claude Code skill (uses CLI)
|
||||||
|
└── pnpm-workspace.yaml
|
||||||
|
```
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
### Main App
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev # Start dev server with Turbopack (localhost:3000)
|
pnpm dev # Start dev server with Turbopack (localhost:3000)
|
||||||
pnpm build # Production build
|
pnpm build # Production build
|
||||||
@@ -19,11 +42,26 @@ pnpm db:studio # Open Drizzle Studio GUI
|
|||||||
bash scripts/deploy.sh # Build locally, rsync to server, restart PM2
|
bash scripts/deploy.sh # Build locally, rsync to server, restart PM2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### CLI Package
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter claw-market build # Build CLI
|
||||||
|
pnpm --filter claw-market lint # Type check CLI
|
||||||
|
pnpm --filter claw-market dev # Build with watch mode
|
||||||
|
|
||||||
|
# Or from packages/claw-market:
|
||||||
|
cd packages/claw-market
|
||||||
|
pnpm build
|
||||||
|
node dist/index.js --help
|
||||||
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Data Flow
|
### Data Flow
|
||||||
|
|
||||||
1. **Reporter skill** (`skill/openclaw-reporter/SKILL.md`) runs inside Claude Code sessions on user machines. It calls `/api/v1/register` once, then sends periodic heartbeats and task reports via Bearer token auth.
|
1. **Reporter sources**:
|
||||||
|
- **CLI** (`packages/claw-market/`) — Standalone npm package `@ricardweii/claw-market`
|
||||||
|
- **Skill** (`skill/openclaw-reporter/SKILL.md`) — Claude Code skill that wraps the CLI
|
||||||
2. **API routes** (`app/api/v1/`) validate requests, update MySQL via Drizzle, cache state in Redis, and publish events to a Redis Pub/Sub channel (`channel:realtime`).
|
2. **API routes** (`app/api/v1/`) validate requests, update MySQL via Drizzle, cache state in Redis, and publish events to a Redis Pub/Sub channel (`channel:realtime`).
|
||||||
3. **SSE endpoint** (`/api/v1/stream`) subscribes to Redis Pub/Sub and streams events to the browser.
|
3. **SSE endpoint** (`/api/v1/stream`) subscribes to Redis Pub/Sub and streams events to the browser.
|
||||||
4. **Frontend** is a single-page "use client" app. The homepage renders a 3D globe (`react-globe.gl`), dashboard panels, and a continent drill-down page. Data arrives via polling (`use-heatmap-data`) and SSE (`use-sse`).
|
4. **Frontend** is a single-page "use client" app. The homepage renders a 3D globe (`react-globe.gl`), dashboard panels, and a continent drill-down page. Data arrives via polling (`use-heatmap-data`) and SSE (`use-sse`).
|
||||||
@@ -33,7 +71,7 @@ bash scripts/deploy.sh # Build locally, rsync to server, restart PM2
|
|||||||
- **Auth**: Bearer token via `lib/auth/api-key.ts`. API keys are generated at registration, cached in Redis for 1 hour.
|
- **Auth**: Bearer token via `lib/auth/api-key.ts`. API keys are generated at registration, cached in Redis for 1 hour.
|
||||||
- **Geo**: IP geolocation via `ip-api.com`, results cached in `geo_cache` MySQL table. Country-to-continent mapping in `lib/geo/ip-location.ts`.
|
- **Geo**: IP geolocation via `ip-api.com`, results cached in `geo_cache` MySQL table. Country-to-continent mapping in `lib/geo/ip-location.ts`.
|
||||||
- **Real-time**: Redis Pub/Sub (`lib/redis/index.ts`) for event broadcasting. SSE stream route creates a per-connection Redis subscriber.
|
- **Real-time**: Redis Pub/Sub (`lib/redis/index.ts`) for event broadcasting. SSE stream route creates a per-connection Redis subscriber.
|
||||||
- **Validation**: Zod schemas in `lib/validators/schemas.ts`.
|
- **Validation**: Zod schemas in `lib/validators/schemas.ts`. Shared with CLI via copy (same structure).
|
||||||
- **Database**: Drizzle ORM with MySQL (`mysql2` driver). Schema in `lib/db/schema.ts`. Tables: `claws`, `heartbeats`, `tasks`, `geo_cache`.
|
- **Database**: Drizzle ORM with MySQL (`mysql2` driver). Schema in `lib/db/schema.ts`. Tables: `claws`, `heartbeats`, `tasks`, `geo_cache`.
|
||||||
- **Redis**: ioredis with two singleton clients (main + subscriber). Stores online status, active claw sorted sets, global/region stats, hourly activity, heatmap cache.
|
- **Redis**: ioredis with two singleton clients (main + subscriber). Stores online status, active claw sorted sets, global/region stats, hourly activity, heatmap cache.
|
||||||
- **i18n**: `next-intl` with locale-prefixed routing (`/en/...`, `/zh/...`). Config in `i18n/routing.ts`, middleware in `middleware.ts`, translations in `messages/en.json` and `messages/zh.json`.
|
- **i18n**: `next-intl` with locale-prefixed routing (`/en/...`, `/zh/...`). Config in `i18n/routing.ts`, middleware in `middleware.ts`, translations in `messages/en.json` and `messages/zh.json`.
|
||||||
@@ -48,6 +86,16 @@ bash scripts/deploy.sh # Build locally, rsync to server, restart PM2
|
|||||||
- `components/layout/` — Navbar, particle background, view switcher, install banner, language switcher
|
- `components/layout/` — Navbar, particle background, view switcher, install banner, language switcher
|
||||||
- `messages/` — i18n translation files (en, zh)
|
- `messages/` — i18n translation files (en, zh)
|
||||||
|
|
||||||
|
### CLI Structure (`packages/claw-market/`)
|
||||||
|
|
||||||
|
- `src/index.ts` — CLI entry point (commander)
|
||||||
|
- `src/commands/` — Command implementations (register, heartbeat, task, config)
|
||||||
|
- `src/lib/api.ts` — HTTP client for API calls
|
||||||
|
- `src/lib/config.ts` — Config file management (~/.openclaw/config.json)
|
||||||
|
- `src/lib/validate.ts` — Zod validation schemas
|
||||||
|
- `src/lib/platform.ts` — Platform/model detection
|
||||||
|
- `src/i18n/` — Translation files (en, zh)
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
See `.env.example`: `DATABASE_URL` (MySQL), `REDIS_URL`, `IP_API_URL`, `NEXT_PUBLIC_APP_URL`.
|
See `.env.example`: `DATABASE_URL` (MySQL), `REDIS_URL`, `IP_API_URL`, `NEXT_PUBLIC_APP_URL`.
|
||||||
@@ -55,3 +103,18 @@ See `.env.example`: `DATABASE_URL` (MySQL), `REDIS_URL`, `IP_API_URL`, `NEXT_PUB
|
|||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
Production runs on a remote server via `scripts/deploy.sh` — builds locally, rsyncs to server, installs prod deps, restarts via PM2 on port 3003.
|
Production runs on a remote server via `scripts/deploy.sh` — builds locally, rsyncs to server, installs prod deps, restarts via PM2 on port 3003.
|
||||||
|
|
||||||
|
## CLI Publishing
|
||||||
|
|
||||||
|
The CLI is published to npm as `@ricardweii/claw-market`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/claw-market
|
||||||
|
npm publish --access public --otp=<OTP>
|
||||||
|
```
|
||||||
|
|
||||||
|
Install globally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @ricardweii/claw-market
|
||||||
|
```
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
clawId,
|
clawId,
|
||||||
apiKey,
|
apiKey,
|
||||||
|
name,
|
||||||
endpoint: `${process.env.NEXT_PUBLIC_APP_URL}/api/v1`,
|
endpoint: `${process.env.NEXT_PUBLIC_APP_URL}/api/v1`,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
"name": "openclaw-market",
|
"name": "openclaw-market",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"workspaces": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
|
|||||||
4
packages/claw-market/.gitignore
vendored
Normal file
4
packages/claw-market/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
163
packages/claw-market/README.md
Normal file
163
packages/claw-market/README.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# @ricardweii/claw-market
|
||||||
|
|
||||||
|
CLI tool for OpenClaw Market - report AI agent activity to the global heatmap.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @ricardweii/claw-market
|
||||||
|
# or
|
||||||
|
pnpm add -g @ricardweii/claw-market
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Register your claw
|
||||||
|
claw-market register MyClaw
|
||||||
|
|
||||||
|
# Send a heartbeat
|
||||||
|
claw-market heartbeat
|
||||||
|
|
||||||
|
# Report a completed task
|
||||||
|
claw-market task "Fixed a bug" --duration 45000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `register`
|
||||||
|
|
||||||
|
Register a new claw on the heatmap.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claw-market register <name> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- `name` - Claw display name (1-100 characters)
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `-p, --platform <string>` - Platform identifier (default: auto-detect)
|
||||||
|
- `-m, --model <string>` - Model identifier (default: from env or 'unknown')
|
||||||
|
- `-f, --force` - Force re-registration even if already registered
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
claw-market register CoolClaw
|
||||||
|
claw-market register "NightCrawler" --model claude-opus-4
|
||||||
|
```
|
||||||
|
|
||||||
|
### `heartbeat`
|
||||||
|
|
||||||
|
Send a heartbeat to update online status.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claw-market heartbeat [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `-n, --name <string>` - Update claw name
|
||||||
|
- `-m, --model <string>` - Update model identifier
|
||||||
|
- `-p, --platform <string>` - Update platform identifier
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
claw-market heartbeat
|
||||||
|
claw-market heartbeat --model claude-sonnet-4
|
||||||
|
```
|
||||||
|
|
||||||
|
### `task`
|
||||||
|
|
||||||
|
Report a completed task.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claw-market task <summary> --duration <ms> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- `summary` - Task summary (max 500 characters)
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `-d, --duration <ms>` - Task duration in milliseconds (required)
|
||||||
|
- `-m, --model <string>` - Model used for the task
|
||||||
|
- `-t, --tools <tools...>` - Tools used (space-separated)
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
claw-market task "Fixed bug" --duration 45000
|
||||||
|
claw-market task "Refactored API" --duration 120000 --tools Bash Read Write
|
||||||
|
```
|
||||||
|
|
||||||
|
### `config`
|
||||||
|
|
||||||
|
Manage CLI configuration.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claw-market config <action>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Actions:**
|
||||||
|
- `show` - Show current configuration
|
||||||
|
- `set <key> <value>` - Set a configuration value
|
||||||
|
- `path` - Show configuration file path
|
||||||
|
- `clear` - Delete configuration (unregister)
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
claw-market config show
|
||||||
|
claw-market config set endpoint https://custom.server/api/v1
|
||||||
|
claw-market config set lang zh
|
||||||
|
claw-market config clear
|
||||||
|
```
|
||||||
|
|
||||||
|
## Global Options
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claw-market [command] [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-e, --endpoint <url> API endpoint (default: https://kymr.top/api/v1)
|
||||||
|
-l, --lang <locale> Output language (en/zh)
|
||||||
|
--json Output in JSON format
|
||||||
|
--version Show version
|
||||||
|
--help Show help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configuration is stored at `~/.openclaw/config.json` with file permissions `600` (owner only).
|
||||||
|
|
||||||
|
**Configuration structure:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"clawId": "abc123...",
|
||||||
|
"apiKey": "a1b2c3...",
|
||||||
|
"name": "MyClaw",
|
||||||
|
"endpoint": "https://kymr.top/api/v1",
|
||||||
|
"lang": "en"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `CLAUDE_MODEL` | Default model identifier |
|
||||||
|
| `OPENCLAW_LANG` | Default output language (en/zh) |
|
||||||
|
|
||||||
|
## Data Disclosure
|
||||||
|
|
||||||
|
This CLI sends the following data to the OpenClaw Market server:
|
||||||
|
|
||||||
|
| Data Field | Example | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| Claw Name | `CoolClaw` | Your display name on the heatmap |
|
||||||
|
| Platform | `darwin`, `linux` | OS type for heatmap stats |
|
||||||
|
| Model | `claude-sonnet-4-6` | Model usage stats |
|
||||||
|
| Task summary | `"Completed a task"` | Generic activity indicator |
|
||||||
|
|
||||||
|
**Never sent:** system usernames, file paths, code snippets, project names, or secrets.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
163
packages/claw-market/README.zh.md
Normal file
163
packages/claw-market/README.zh.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# @ricardweii/claw-market
|
||||||
|
|
||||||
|
OpenClaw Market 命令行工具 - 向全球热力图报告 AI agent 活动。
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @ricardweii/claw-market
|
||||||
|
# 或
|
||||||
|
pnpm add -g @ricardweii/claw-market
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 注册你的 claw
|
||||||
|
claw-market register 我的龙虾
|
||||||
|
|
||||||
|
# 发送心跳
|
||||||
|
claw-market heartbeat
|
||||||
|
|
||||||
|
# 报告已完成的任务
|
||||||
|
claw-market task "修复了一个 bug" --duration 45000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 命令
|
||||||
|
|
||||||
|
### `register`
|
||||||
|
|
||||||
|
在热力图上注册一个新的 claw。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claw-market register <name> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `name` - Claw 显示名称 (1-100 字符)
|
||||||
|
|
||||||
|
**选项:**
|
||||||
|
- `-p, --platform <string>` - 平台标识 (默认: 自动检测)
|
||||||
|
- `-m, --model <string>` - 模型标识 (默认: 从环境变量或 'unknown')
|
||||||
|
- `-f, --force` - 强制重新注册 (即使已注册)
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```bash
|
||||||
|
claw-market register 酷龙虾
|
||||||
|
claw-market register "暗夜龙虾" --model claude-opus-4
|
||||||
|
```
|
||||||
|
|
||||||
|
### `heartbeat`
|
||||||
|
|
||||||
|
发送心跳以更新在线状态。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claw-market heartbeat [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**选项:**
|
||||||
|
- `-n, --name <string>` - 更新 claw 名称
|
||||||
|
- `-m, --model <string>` - 更新模型标识
|
||||||
|
- `-p, --platform <string>` - 更新平台标识
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```bash
|
||||||
|
claw-market heartbeat
|
||||||
|
claw-market heartbeat --model claude-sonnet-4
|
||||||
|
```
|
||||||
|
|
||||||
|
### `task`
|
||||||
|
|
||||||
|
报告已完成的任务。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claw-market task <summary> --duration <ms> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `summary` - 任务摘要 (最多 500 字符)
|
||||||
|
|
||||||
|
**选项:**
|
||||||
|
- `-d, --duration <ms>` - 任务时长 (毫秒,必填)
|
||||||
|
- `-m, --model <string>` - 任务使用的模型
|
||||||
|
- `-t, --tools <tools...>` - 使用的工具 (空格分隔)
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```bash
|
||||||
|
claw-market task "修复 bug" --duration 45000
|
||||||
|
claw-market task "重构 API" --duration 120000 --tools Bash Read Write
|
||||||
|
```
|
||||||
|
|
||||||
|
### `config`
|
||||||
|
|
||||||
|
管理 CLI 配置。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claw-market config <action>
|
||||||
|
```
|
||||||
|
|
||||||
|
**操作:**
|
||||||
|
- `show` - 显示当前配置
|
||||||
|
- `set <key> <value>` - 设置配置值
|
||||||
|
- `path` - 显示配置文件路径
|
||||||
|
- `clear` - 清除配置 (注销)
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```bash
|
||||||
|
claw-market config show
|
||||||
|
claw-market config set endpoint https://custom.server/api/v1
|
||||||
|
claw-market config set lang zh
|
||||||
|
claw-market config clear
|
||||||
|
```
|
||||||
|
|
||||||
|
## 全局选项
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claw-market [command] [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-e, --endpoint <url> API 端点 (默认: https://kymr.top/api/v1)
|
||||||
|
-l, --lang <locale> 输出语言 (en/zh)
|
||||||
|
--json 以 JSON 格式输出
|
||||||
|
--version 显示版本
|
||||||
|
--help 显示帮助
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
配置存储在 `~/.openclaw/config.json`,文件权限为 `600` (仅所有者可读写)。
|
||||||
|
|
||||||
|
**配置结构:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"clawId": "abc123...",
|
||||||
|
"apiKey": "a1b2c3...",
|
||||||
|
"name": "我的龙虾",
|
||||||
|
"endpoint": "https://kymr.top/api/v1",
|
||||||
|
"lang": "zh"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
| 变量 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| `CLAUDE_MODEL` | 默认模型标识 |
|
||||||
|
| `OPENCLAW_LANG` | 默认输出语言 (en/zh) |
|
||||||
|
|
||||||
|
## 数据披露
|
||||||
|
|
||||||
|
此 CLI 向 OpenClaw Market 服务器发送以下数据:
|
||||||
|
|
||||||
|
| 数据字段 | 示例 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| Claw 名称 | `酷龙虾` | 热力图上的显示名称 |
|
||||||
|
| 平台 | `darwin`, `linux` | 热力图统计的操作系统类型 |
|
||||||
|
| 模型 | `claude-sonnet-4-6` | 模型使用统计 |
|
||||||
|
| 任务摘要 | `"完成了一个任务"` | 通用活动指标 |
|
||||||
|
|
||||||
|
**永不发送:** 系统用户名、文件路径、代码片段、项目名称或密钥。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT
|
||||||
62
packages/claw-market/package.json
Normal file
62
packages/claw-market/package.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"name": "@ricardweii/claw-market",
|
||||||
|
"version": "0.1.1",
|
||||||
|
"description": "CLI tool for OpenClaw Market - report AI agent activity to the global heatmap",
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"claw-market": "dist/index.js"
|
||||||
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md",
|
||||||
|
"README.zh.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"dev": "tsup --watch",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"lint": "tsc --noEmit",
|
||||||
|
"prepublishOnly": "pnpm build"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"cli",
|
||||||
|
"ai",
|
||||||
|
"agent",
|
||||||
|
"heatmap",
|
||||||
|
"openclaw",
|
||||||
|
"telemetry"
|
||||||
|
],
|
||||||
|
"author": "OpenClaw",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/openclaw/openclaw-market.git",
|
||||||
|
"directory": "packages/claw-market"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/openclaw/openclaw-market/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://kymr.top",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^13.1.0",
|
||||||
|
"zod": "^3.24.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.13.0",
|
||||||
|
"tsup": "^8.3.0",
|
||||||
|
"typescript": "^5.8.0",
|
||||||
|
"vitest": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
117
packages/claw-market/src/commands/config.ts
Normal file
117
packages/claw-market/src/commands/config.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import type { Command } from "commander";
|
||||||
|
import {
|
||||||
|
readConfig,
|
||||||
|
getConfigPath,
|
||||||
|
clearConfig,
|
||||||
|
updateConfig,
|
||||||
|
isValidConfigKey,
|
||||||
|
VALID_CONFIG_KEYS,
|
||||||
|
} from "../lib/config.js";
|
||||||
|
import type { Translator, Locale } from "../i18n/index.js";
|
||||||
|
|
||||||
|
export interface ConfigOptions {
|
||||||
|
lang?: Locale;
|
||||||
|
json?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function configCommand(program: Command, t: Translator): void {
|
||||||
|
const configCmd = program
|
||||||
|
.command("config")
|
||||||
|
.description(t("config.description"));
|
||||||
|
|
||||||
|
// config show
|
||||||
|
configCmd
|
||||||
|
.command("show")
|
||||||
|
.description(t("config.showDescription"))
|
||||||
|
.action((options: ConfigOptions, cmd: Command) => {
|
||||||
|
const globalOpts = cmd.optsWithGlobals();
|
||||||
|
const json = globalOpts.json;
|
||||||
|
|
||||||
|
const config = readConfig();
|
||||||
|
if (!config) {
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: false, error: "not_registered" }));
|
||||||
|
} else {
|
||||||
|
console.log(t("config.notRegistered"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: true, config }, null, 2));
|
||||||
|
} else {
|
||||||
|
console.log(t("config.currentConfig"));
|
||||||
|
console.log(` clawId: ${config.clawId}`);
|
||||||
|
console.log(` name: ${config.name}`);
|
||||||
|
if (config.endpoint) console.log(` endpoint: ${config.endpoint}`);
|
||||||
|
if (config.lang) console.log(` lang: ${config.lang}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// config set
|
||||||
|
configCmd
|
||||||
|
.command("set <key> <value>")
|
||||||
|
.description(t("config.setDescription"))
|
||||||
|
.action((key: string, value: string, options: ConfigOptions, cmd: Command) => {
|
||||||
|
const globalOpts = cmd.optsWithGlobals();
|
||||||
|
const json = globalOpts.json;
|
||||||
|
|
||||||
|
if (!isValidConfigKey(key)) {
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: false, error: "invalid_key" }));
|
||||||
|
} else {
|
||||||
|
console.log(t("config.invalidKey", { key, validKeys: VALID_CONFIG_KEYS.join(", ") }));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = readConfig();
|
||||||
|
if (!config) {
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: false, error: "not_registered" }));
|
||||||
|
} else {
|
||||||
|
console.log(t("config.notRegistered"));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConfig({ [key]: value });
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: true, key, value }));
|
||||||
|
} else {
|
||||||
|
console.log(t("config.setValue", { key, value }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// config path
|
||||||
|
configCmd
|
||||||
|
.command("path")
|
||||||
|
.description(t("config.pathDescription"))
|
||||||
|
.action((options: ConfigOptions, cmd: Command) => {
|
||||||
|
const globalOpts = cmd.optsWithGlobals();
|
||||||
|
const json = globalOpts.json;
|
||||||
|
|
||||||
|
const configPath = getConfigPath();
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: true, path: configPath }));
|
||||||
|
} else {
|
||||||
|
console.log(t("config.configPath", { path: configPath }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// config clear
|
||||||
|
configCmd
|
||||||
|
.command("clear")
|
||||||
|
.description(t("config.clearDescription"))
|
||||||
|
.action((options: ConfigOptions, cmd: Command) => {
|
||||||
|
const globalOpts = cmd.optsWithGlobals();
|
||||||
|
const json = globalOpts.json;
|
||||||
|
|
||||||
|
const success = clearConfig();
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success }));
|
||||||
|
} else {
|
||||||
|
console.log(t("config.configCleared"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
74
packages/claw-market/src/commands/heartbeat.ts
Normal file
74
packages/claw-market/src/commands/heartbeat.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import type { Command } from "commander";
|
||||||
|
import { readConfig, updateConfig } from "../lib/config.js";
|
||||||
|
import { sendHeartbeat, getEndpoint } from "../lib/api.js";
|
||||||
|
import { detectPlatform, detectModel } from "../lib/platform.js";
|
||||||
|
import type { Translator, Locale } from "../i18n/index.js";
|
||||||
|
|
||||||
|
export interface HeartbeatOptions {
|
||||||
|
name?: string;
|
||||||
|
model?: string;
|
||||||
|
platform?: string;
|
||||||
|
endpoint?: string;
|
||||||
|
lang?: Locale;
|
||||||
|
json?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function heartbeatCommand(program: Command, t: Translator): void {
|
||||||
|
program
|
||||||
|
.command("heartbeat")
|
||||||
|
.description(t("heartbeat.description"))
|
||||||
|
.option("-n, --name <string>", t("heartbeat.optionName"))
|
||||||
|
.option("-m, --model <string>", t("heartbeat.optionModel"))
|
||||||
|
.option("-p, --platform <string>", t("heartbeat.optionPlatform"))
|
||||||
|
.action(async (options: HeartbeatOptions, cmd: Command) => {
|
||||||
|
// Get global options
|
||||||
|
const globalOpts = cmd.optsWithGlobals();
|
||||||
|
const json = globalOpts.json;
|
||||||
|
|
||||||
|
// Check if registered
|
||||||
|
const config = readConfig();
|
||||||
|
if (!config) {
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: false, error: "not_registered" }));
|
||||||
|
} else {
|
||||||
|
console.error(t("heartbeat.notRegistered"));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get endpoint
|
||||||
|
const endpoint = getEndpoint(config, options.endpoint);
|
||||||
|
|
||||||
|
// Prepare heartbeat data
|
||||||
|
const heartbeatData = {
|
||||||
|
endpoint,
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
name: options.name,
|
||||||
|
model: options.model || detectModel(),
|
||||||
|
platform: options.platform || detectPlatform(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call API
|
||||||
|
const result = await sendHeartbeat(heartbeatData);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: false, error: result.error }));
|
||||||
|
} else {
|
||||||
|
console.error(t("heartbeat.error", { error: result.error ?? "Unknown error" }));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update config if name changed
|
||||||
|
if (options.name && options.name !== config.name) {
|
||||||
|
updateConfig({ name: options.name });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: true }));
|
||||||
|
} else {
|
||||||
|
console.log(t("heartbeat.success"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
87
packages/claw-market/src/commands/register.ts
Normal file
87
packages/claw-market/src/commands/register.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import type { Command } from "commander";
|
||||||
|
import { configExists, readConfig, writeConfig } from "../lib/config.js";
|
||||||
|
import { registerClaw, getEndpoint } from "../lib/api.js";
|
||||||
|
import { detectPlatform, detectModel } from "../lib/platform.js";
|
||||||
|
import { validateRegister } from "../lib/validate.js";
|
||||||
|
import type { Translator, Locale } from "../i18n/index.js";
|
||||||
|
|
||||||
|
export interface RegisterOptions {
|
||||||
|
platform?: string;
|
||||||
|
model?: string;
|
||||||
|
force?: boolean;
|
||||||
|
endpoint?: string;
|
||||||
|
lang?: Locale;
|
||||||
|
json?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerCommand(program: Command, t: Translator): void {
|
||||||
|
program
|
||||||
|
.command("register <name>")
|
||||||
|
.description(t("register.description"))
|
||||||
|
.option("-p, --platform <string>", t("register.optionPlatform"))
|
||||||
|
.option("-m, --model <string>", t("register.optionModel"))
|
||||||
|
.option("-f, --force", t("register.optionForce"))
|
||||||
|
.action(async (name: string, options: RegisterOptions, cmd: Command) => {
|
||||||
|
// Get global options
|
||||||
|
const globalOpts = cmd.optsWithGlobals();
|
||||||
|
const json = globalOpts.json;
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
const validation = validateRegister({ name, ...options });
|
||||||
|
if (!validation.success) {
|
||||||
|
console.error(t("register.error", { error: validation.error }));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already registered
|
||||||
|
const existingConfig = readConfig();
|
||||||
|
if (existingConfig && !options.force) {
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: false, error: "already_registered", name: existingConfig.name }));
|
||||||
|
} else {
|
||||||
|
console.log(t("register.alreadyRegistered", { name: existingConfig.name }));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get endpoint
|
||||||
|
const endpoint = getEndpoint(existingConfig, globalOpts.endpoint);
|
||||||
|
|
||||||
|
// Prepare registration data
|
||||||
|
const platform = options.platform || detectPlatform();
|
||||||
|
const model = options.model || detectModel();
|
||||||
|
|
||||||
|
// Call API
|
||||||
|
const result = await registerClaw({
|
||||||
|
endpoint,
|
||||||
|
name: validation.data.name,
|
||||||
|
platform,
|
||||||
|
model,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: false, error: result.error }));
|
||||||
|
} else {
|
||||||
|
console.error(t("register.error", { error: result.error ?? "Unknown error" }));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save config
|
||||||
|
const config = {
|
||||||
|
clawId: result.data!.clawId,
|
||||||
|
apiKey: result.data!.apiKey,
|
||||||
|
name: result.data!.name,
|
||||||
|
endpoint: globalOpts.endpoint,
|
||||||
|
lang: globalOpts.lang,
|
||||||
|
};
|
||||||
|
writeConfig(config);
|
||||||
|
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: true, ...result.data }));
|
||||||
|
} else {
|
||||||
|
console.log(t("register.success", { name: result.data!.name }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
95
packages/claw-market/src/commands/task.ts
Normal file
95
packages/claw-market/src/commands/task.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import type { Command } from "commander";
|
||||||
|
import { readConfig } from "../lib/config.js";
|
||||||
|
import { reportTask, getEndpoint } from "../lib/api.js";
|
||||||
|
import { validateTask } from "../lib/validate.js";
|
||||||
|
import { detectModel } from "../lib/platform.js";
|
||||||
|
import type { Translator, Locale } from "../i18n/index.js";
|
||||||
|
|
||||||
|
export interface TaskOptions {
|
||||||
|
duration?: number;
|
||||||
|
model?: string;
|
||||||
|
tools?: string[];
|
||||||
|
endpoint?: string;
|
||||||
|
lang?: Locale;
|
||||||
|
json?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function taskCommand(program: Command, t: Translator): void {
|
||||||
|
program
|
||||||
|
.command("task <summary>")
|
||||||
|
.description(t("task.description"))
|
||||||
|
.requiredOption("-d, --duration <ms>", t("task.optionDuration"), (value) => parseInt(value, 10))
|
||||||
|
.option("-m, --model <string>", t("task.optionModel"))
|
||||||
|
.option("-t, --tools <tools...>", t("task.optionTools"))
|
||||||
|
.action(async (summary: string, options: TaskOptions, cmd: Command) => {
|
||||||
|
// Get global options
|
||||||
|
const globalOpts = cmd.optsWithGlobals();
|
||||||
|
const json = globalOpts.json;
|
||||||
|
|
||||||
|
// Check if registered
|
||||||
|
const config = readConfig();
|
||||||
|
if (!config) {
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: false, error: "not_registered" }));
|
||||||
|
} else {
|
||||||
|
console.error(t("task.notRegistered"));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate duration
|
||||||
|
if (!options.duration || options.duration <= 0) {
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: false, error: "invalid_duration" }));
|
||||||
|
} else {
|
||||||
|
console.error(t("task.durationRequired"));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
const validation = validateTask({
|
||||||
|
summary,
|
||||||
|
durationMs: options.duration,
|
||||||
|
model: options.model,
|
||||||
|
toolsUsed: options.tools,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!validation.success) {
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: false, error: validation.error }));
|
||||||
|
} else {
|
||||||
|
console.error(t("task.error", { error: validation.error }));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get endpoint
|
||||||
|
const endpoint = getEndpoint(config, globalOpts.endpoint);
|
||||||
|
|
||||||
|
// Call API
|
||||||
|
const result = await reportTask({
|
||||||
|
endpoint,
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
summary: validation.data.summary,
|
||||||
|
durationMs: validation.data.durationMs,
|
||||||
|
model: validation.data.model || detectModel(),
|
||||||
|
toolsUsed: validation.data.toolsUsed,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: false, error: result.error }));
|
||||||
|
} else {
|
||||||
|
console.error(t("task.error", { error: result.error ?? "Unknown error" }));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json) {
|
||||||
|
console.log(JSON.stringify({ success: true }));
|
||||||
|
} else {
|
||||||
|
console.log(t("task.success"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
61
packages/claw-market/src/i18n/en.json
Normal file
61
packages/claw-market/src/i18n/en.json
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"cli": {
|
||||||
|
"description": "CLI tool for OpenClaw Market - report AI agent activity to the global heatmap"
|
||||||
|
},
|
||||||
|
"register": {
|
||||||
|
"description": "Register a new claw on the heatmap",
|
||||||
|
"argName": "Claw display name (1-100 characters)",
|
||||||
|
"optionPlatform": "Platform identifier (default: auto-detect)",
|
||||||
|
"optionModel": "Model identifier (default: from env or 'unknown')",
|
||||||
|
"optionForce": "Force re-registration even if already registered",
|
||||||
|
"success": "Registered successfully as: {name}",
|
||||||
|
"alreadyRegistered": "Already registered as: {name}. Use --force to re-register.",
|
||||||
|
"error": "Registration failed: {error}",
|
||||||
|
"nameRequired": "Error: Claw name is required"
|
||||||
|
},
|
||||||
|
"heartbeat": {
|
||||||
|
"description": "Send a heartbeat to update online status",
|
||||||
|
"optionName": "Update claw name",
|
||||||
|
"optionModel": "Update model identifier",
|
||||||
|
"optionPlatform": "Update platform identifier",
|
||||||
|
"success": "Heartbeat sent successfully",
|
||||||
|
"error": "Heartbeat failed: {error}",
|
||||||
|
"notRegistered": "Error: Not registered. Run 'claw-market register' first."
|
||||||
|
},
|
||||||
|
"task": {
|
||||||
|
"description": "Report a completed task",
|
||||||
|
"argSummary": "Task summary (max 500 characters)",
|
||||||
|
"optionDuration": "Task duration in milliseconds (required)",
|
||||||
|
"optionModel": "Model used for the task",
|
||||||
|
"optionTools": "Tools used (space-separated)",
|
||||||
|
"success": "Task reported successfully",
|
||||||
|
"error": "Task report failed: {error}",
|
||||||
|
"notRegistered": "Error: Not registered. Run 'claw-market register' first.",
|
||||||
|
"summaryRequired": "Error: Task summary is required",
|
||||||
|
"durationRequired": "Error: --duration is required"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"description": "Manage CLI configuration",
|
||||||
|
"showDescription": "Show current configuration",
|
||||||
|
"setDescription": "Set a configuration value",
|
||||||
|
"pathDescription": "Show configuration file path",
|
||||||
|
"clearDescription": "Clear configuration (unregister)",
|
||||||
|
"argKey": "Configuration key",
|
||||||
|
"argValue": "Configuration value",
|
||||||
|
"currentConfig": "Current configuration:",
|
||||||
|
"configPath": "Configuration file: {path}",
|
||||||
|
"configCleared": "Configuration cleared successfully",
|
||||||
|
"notRegistered": "No configuration found. Run 'claw-market register' first.",
|
||||||
|
"invalidKey": "Invalid configuration key: {key}. Valid keys: {validKeys}",
|
||||||
|
"setValue": "Set {key} = {value}"
|
||||||
|
},
|
||||||
|
"global": {
|
||||||
|
"optionEndpoint": "API endpoint (default: https://kymr.top/api/v1)",
|
||||||
|
"optionLang": "Output language (en/zh)",
|
||||||
|
"optionJson": "Output in JSON format",
|
||||||
|
"optionVersion": "Show version",
|
||||||
|
"optionHelp": "Show help",
|
||||||
|
"unknownError": "An unexpected error occurred: {error}",
|
||||||
|
"networkError": "Network error: Unable to connect to {endpoint}"
|
||||||
|
}
|
||||||
|
}
|
||||||
70
packages/claw-market/src/i18n/index.ts
Normal file
70
packages/claw-market/src/i18n/index.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import en from "./en.json" with { type: "json" };
|
||||||
|
import zh from "./zh.json" with { type: "json" };
|
||||||
|
|
||||||
|
export type Locale = "en" | "zh";
|
||||||
|
export type TranslationKeys = typeof en;
|
||||||
|
|
||||||
|
const translations: Record<Locale, TranslationKeys> = { en, zh };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect the user's preferred language
|
||||||
|
* Priority: --lang option > OPENCLAW_LANG env > config file > system LANG > default 'en'
|
||||||
|
*/
|
||||||
|
export function detectLocale(override?: Locale): Locale {
|
||||||
|
if (override) return override;
|
||||||
|
|
||||||
|
const envLang = process.env.OPENCLAW_LANG;
|
||||||
|
if (envLang === "en" || envLang === "zh") return envLang;
|
||||||
|
|
||||||
|
const systemLang = process.env.LANG || process.env.LC_ALL || "";
|
||||||
|
if (systemLang.toLowerCase().includes("zh")) return "zh";
|
||||||
|
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get translation value by dot-notation key
|
||||||
|
*/
|
||||||
|
function getNestedValue(obj: Record<string, unknown>, path: string): string | undefined {
|
||||||
|
const parts = path.split(".");
|
||||||
|
let current: unknown = obj;
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
if (current && typeof current === "object" && part in current) {
|
||||||
|
current = (current as Record<string, unknown>)[part];
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof current === "string" ? current : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace template variables in translation string
|
||||||
|
*/
|
||||||
|
function interpolate(template: string, vars: Record<string, string | number>): string {
|
||||||
|
return template.replace(/\{(\w+)\}/g, (_, key) => String(vars[key] ?? `{${key}}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translation function
|
||||||
|
*/
|
||||||
|
export function t(key: string, locale: Locale, vars?: Record<string, string | number>): string {
|
||||||
|
const translation = getNestedValue(translations[locale] as unknown as Record<string, unknown>, key);
|
||||||
|
const text = translation ?? key;
|
||||||
|
|
||||||
|
if (vars) {
|
||||||
|
return interpolate(text, vars);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a translation function bound to a specific locale
|
||||||
|
*/
|
||||||
|
export function createTranslator(locale: Locale) {
|
||||||
|
return (key: string, vars?: Record<string, string | number>) => t(key, locale, vars);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Translator = ReturnType<typeof createTranslator>;
|
||||||
61
packages/claw-market/src/i18n/zh.json
Normal file
61
packages/claw-market/src/i18n/zh.json
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"cli": {
|
||||||
|
"description": "OpenClaw Market 命令行工具 - 向全球热力图报告 AI agent 活动"
|
||||||
|
},
|
||||||
|
"register": {
|
||||||
|
"description": "在热力图上注册一个新的 claw",
|
||||||
|
"argName": "Claw 显示名称 (1-100 字符)",
|
||||||
|
"optionPlatform": "平台标识 (默认: 自动检测)",
|
||||||
|
"optionModel": "模型标识 (默认: 从环境变量或 'unknown')",
|
||||||
|
"optionForce": "强制重新注册 (即使已注册)",
|
||||||
|
"success": "注册成功: {name}",
|
||||||
|
"alreadyRegistered": "已注册为: {name}。使用 --force 强制重新注册。",
|
||||||
|
"error": "注册失败: {error}",
|
||||||
|
"nameRequired": "错误: 需要提供 claw 名称"
|
||||||
|
},
|
||||||
|
"heartbeat": {
|
||||||
|
"description": "发送心跳以更新在线状态",
|
||||||
|
"optionName": "更新 claw 名称",
|
||||||
|
"optionModel": "更新模型标识",
|
||||||
|
"optionPlatform": "更新平台标识",
|
||||||
|
"success": "心跳发送成功",
|
||||||
|
"error": "心跳发送失败: {error}",
|
||||||
|
"notRegistered": "错误: 未注册。请先运行 'claw-market register'。"
|
||||||
|
},
|
||||||
|
"task": {
|
||||||
|
"description": "报告已完成的任务",
|
||||||
|
"argSummary": "任务摘要 (最多 500 字符)",
|
||||||
|
"optionDuration": "任务时长 (毫秒,必填)",
|
||||||
|
"optionModel": "任务使用的模型",
|
||||||
|
"optionTools": "使用的工具 (空格分隔)",
|
||||||
|
"success": "任务报告成功",
|
||||||
|
"error": "任务报告失败: {error}",
|
||||||
|
"notRegistered": "错误: 未注册。请先运行 'claw-market register'。",
|
||||||
|
"summaryRequired": "错误: 需要提供任务摘要",
|
||||||
|
"durationRequired": "错误: --duration 是必填项"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"description": "管理 CLI 配置",
|
||||||
|
"showDescription": "显示当前配置",
|
||||||
|
"setDescription": "设置配置值",
|
||||||
|
"pathDescription": "显示配置文件路径",
|
||||||
|
"clearDescription": "清除配置 (注销)",
|
||||||
|
"argKey": "配置键",
|
||||||
|
"argValue": "配置值",
|
||||||
|
"currentConfig": "当前配置:",
|
||||||
|
"configPath": "配置文件: {path}",
|
||||||
|
"configCleared": "配置已清除",
|
||||||
|
"notRegistered": "未找到配置。请先运行 'claw-market register'。",
|
||||||
|
"invalidKey": "无效的配置键: {key}。有效的键: {validKeys}",
|
||||||
|
"setValue": "已设置 {key} = {value}"
|
||||||
|
},
|
||||||
|
"global": {
|
||||||
|
"optionEndpoint": "API 端点 (默认: https://kymr.top/api/v1)",
|
||||||
|
"optionLang": "输出语言 (en/zh)",
|
||||||
|
"optionJson": "以 JSON 格式输出",
|
||||||
|
"optionVersion": "显示版本",
|
||||||
|
"optionHelp": "显示帮助",
|
||||||
|
"unknownError": "发生意外错误: {error}",
|
||||||
|
"networkError": "网络错误: 无法连接到 {endpoint}"
|
||||||
|
}
|
||||||
|
}
|
||||||
69
packages/claw-market/src/index.ts
Normal file
69
packages/claw-market/src/index.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
import { detectLocale, createTranslator, type Locale } from "./i18n/index.js";
|
||||||
|
import { registerCommand } from "./commands/register.js";
|
||||||
|
import { heartbeatCommand } from "./commands/heartbeat.js";
|
||||||
|
import { taskCommand } from "./commands/task.js";
|
||||||
|
import { configCommand } from "./commands/config.js";
|
||||||
|
import { readConfig } from "./lib/config.js";
|
||||||
|
import { DEFAULT_ENDPOINT } from "./lib/api.js";
|
||||||
|
|
||||||
|
const VERSION = "0.1.1";
|
||||||
|
|
||||||
|
interface GlobalOptions {
|
||||||
|
endpoint?: string;
|
||||||
|
lang?: Locale;
|
||||||
|
json?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main(): void {
|
||||||
|
// Pre-parse to get language option
|
||||||
|
const preParseArgs = process.argv.slice(2);
|
||||||
|
let langOverride: Locale | undefined;
|
||||||
|
let jsonOutput = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < preParseArgs.length; i++) {
|
||||||
|
const arg = preParseArgs[i];
|
||||||
|
if (arg === "--lang" || arg === "-l") {
|
||||||
|
const value = preParseArgs[i + 1];
|
||||||
|
if (value === "en" || value === "zh") {
|
||||||
|
langOverride = value;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
} else if (arg === "--json") {
|
||||||
|
jsonOutput = true;
|
||||||
|
} else if (arg.startsWith("--lang=")) {
|
||||||
|
const value = arg.split("=")[1];
|
||||||
|
if (value === "en" || value === "zh") {
|
||||||
|
langOverride = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect locale
|
||||||
|
const config = readConfig();
|
||||||
|
const locale = detectLocale(langOverride || config?.lang);
|
||||||
|
const t = createTranslator(locale);
|
||||||
|
|
||||||
|
// Create program
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
program
|
||||||
|
.name("claw-market")
|
||||||
|
.description(t("cli.description"))
|
||||||
|
.version(VERSION)
|
||||||
|
.option("-e, --endpoint <url>", t("global.optionEndpoint"), DEFAULT_ENDPOINT)
|
||||||
|
.option("-l, --lang <locale>", t("global.optionLang"))
|
||||||
|
.option("--json", t("global.optionJson"));
|
||||||
|
|
||||||
|
// Add commands
|
||||||
|
registerCommand(program, t);
|
||||||
|
heartbeatCommand(program, t);
|
||||||
|
taskCommand(program, t);
|
||||||
|
configCommand(program, t);
|
||||||
|
|
||||||
|
// Parse
|
||||||
|
program.parse(process.argv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run main
|
||||||
|
main();
|
||||||
176
packages/claw-market/src/lib/api.ts
Normal file
176
packages/claw-market/src/lib/api.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import type { OpenClawConfig } from "./config.js";
|
||||||
|
|
||||||
|
export interface ApiResponse<T = unknown> {
|
||||||
|
success: boolean;
|
||||||
|
data?: T;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterResponse {
|
||||||
|
clawId: string;
|
||||||
|
apiKey: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiError {
|
||||||
|
error: string;
|
||||||
|
statusCode?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default API endpoint
|
||||||
|
*/
|
||||||
|
export const DEFAULT_ENDPOINT = "https://kymr.top/api/v1";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build full URL for API endpoint
|
||||||
|
*/
|
||||||
|
function buildUrl(endpoint: string, path: string): string {
|
||||||
|
const base = endpoint.endsWith("/api/v1") ? endpoint : `${endpoint}/api/v1`;
|
||||||
|
return `${base}${path.startsWith("/") ? path : `/${path}`}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an API request
|
||||||
|
*/
|
||||||
|
async function request<T>(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
options: {
|
||||||
|
endpoint: string;
|
||||||
|
apiKey?: string;
|
||||||
|
body?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
const { endpoint, apiKey, body } = options;
|
||||||
|
const url = buildUrl(endpoint, path);
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": "claw-market-cli/0.1.0",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers["Authorization"] = `Bearer ${apiKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: (data as ApiError).error || `HTTP ${response.status}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: data as T,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new claw
|
||||||
|
*/
|
||||||
|
export async function registerClaw(options: {
|
||||||
|
endpoint: string;
|
||||||
|
name: string;
|
||||||
|
platform?: string;
|
||||||
|
model?: string;
|
||||||
|
}): Promise<ApiResponse<RegisterResponse>> {
|
||||||
|
const body: Record<string, unknown> = { name: options.name };
|
||||||
|
|
||||||
|
if (options.platform) {
|
||||||
|
body.platform = options.platform;
|
||||||
|
}
|
||||||
|
if (options.model) {
|
||||||
|
body.model = options.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request<RegisterResponse>("POST", "/register", {
|
||||||
|
endpoint: options.endpoint,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a heartbeat
|
||||||
|
*/
|
||||||
|
export async function sendHeartbeat(options: {
|
||||||
|
endpoint: string;
|
||||||
|
apiKey: string;
|
||||||
|
name?: string;
|
||||||
|
platform?: string;
|
||||||
|
model?: string;
|
||||||
|
}): Promise<ApiResponse<void>> {
|
||||||
|
const body: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (options.name) {
|
||||||
|
body.name = options.name;
|
||||||
|
}
|
||||||
|
if (options.platform) {
|
||||||
|
body.platform = options.platform;
|
||||||
|
}
|
||||||
|
if (options.model) {
|
||||||
|
body.model = options.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request<void>("POST", "/heartbeat", {
|
||||||
|
endpoint: options.endpoint,
|
||||||
|
apiKey: options.apiKey,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report a completed task
|
||||||
|
*/
|
||||||
|
export async function reportTask(options: {
|
||||||
|
endpoint: string;
|
||||||
|
apiKey: string;
|
||||||
|
summary: string;
|
||||||
|
durationMs: number;
|
||||||
|
model?: string;
|
||||||
|
toolsUsed?: string[];
|
||||||
|
}): Promise<ApiResponse<void>> {
|
||||||
|
const body: Record<string, unknown> = {
|
||||||
|
summary: options.summary,
|
||||||
|
durationMs: options.durationMs,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.model) {
|
||||||
|
body.model = options.model;
|
||||||
|
}
|
||||||
|
if (options.toolsUsed && options.toolsUsed.length > 0) {
|
||||||
|
body.toolsUsed = options.toolsUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request<void>("POST", "/task", {
|
||||||
|
endpoint: options.endpoint,
|
||||||
|
apiKey: options.apiKey,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get endpoint from config or use default
|
||||||
|
*/
|
||||||
|
export function getEndpoint(config: OpenClawConfig | null, override?: string): string {
|
||||||
|
if (override) return override;
|
||||||
|
if (config?.endpoint) return config.endpoint;
|
||||||
|
return DEFAULT_ENDPOINT;
|
||||||
|
}
|
||||||
118
packages/claw-market/src/lib/config.ts
Normal file
118
packages/claw-market/src/lib/config.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, rmdirSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import type { Locale } from "../i18n/index.js";
|
||||||
|
|
||||||
|
export interface OpenClawConfig {
|
||||||
|
clawId: string;
|
||||||
|
apiKey: string;
|
||||||
|
name: string;
|
||||||
|
endpoint?: string;
|
||||||
|
lang?: Locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONFIG_DIR = ".openclaw";
|
||||||
|
const CONFIG_FILE = "config.json";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the configuration directory path
|
||||||
|
*/
|
||||||
|
export function getConfigDir(): string {
|
||||||
|
return join(homedir(), CONFIG_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the configuration file path
|
||||||
|
*/
|
||||||
|
export function getConfigPath(): string {
|
||||||
|
return join(getConfigDir(), CONFIG_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if configuration exists
|
||||||
|
*/
|
||||||
|
export function configExists(): boolean {
|
||||||
|
return existsSync(getConfigPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read configuration from file
|
||||||
|
*/
|
||||||
|
export function readConfig(): OpenClawConfig | null {
|
||||||
|
const configPath = getConfigPath();
|
||||||
|
if (!existsSync(configPath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = readFileSync(configPath, "utf-8");
|
||||||
|
return JSON.parse(content) as OpenClawConfig;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write configuration to file
|
||||||
|
* Creates directory if needed and sets permissions to 600 (owner only)
|
||||||
|
*/
|
||||||
|
export function writeConfig(config: OpenClawConfig): void {
|
||||||
|
const configDir = getConfigDir();
|
||||||
|
const configPath = getConfigPath();
|
||||||
|
|
||||||
|
if (!existsSync(configDir)) {
|
||||||
|
mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(configPath, JSON.stringify(config, null, 2), {
|
||||||
|
encoding: "utf-8",
|
||||||
|
mode: 0o600,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update specific configuration values
|
||||||
|
*/
|
||||||
|
export function updateConfig(updates: Partial<OpenClawConfig>): OpenClawConfig | null {
|
||||||
|
const existing = readConfig();
|
||||||
|
if (!existing) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = { ...existing, ...updates };
|
||||||
|
writeConfig(updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete configuration (unregister)
|
||||||
|
*/
|
||||||
|
export function clearConfig(): boolean {
|
||||||
|
const configPath = getConfigPath();
|
||||||
|
const configDir = getConfigDir();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (existsSync(configPath)) {
|
||||||
|
unlinkSync(configPath);
|
||||||
|
}
|
||||||
|
if (existsSync(configDir)) {
|
||||||
|
rmdirSync(configDir);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid configuration keys for set operation
|
||||||
|
*/
|
||||||
|
export const VALID_CONFIG_KEYS = ["endpoint", "lang"] as const;
|
||||||
|
export type ConfigKey = (typeof VALID_CONFIG_KEYS)[number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a key is a valid configuration key
|
||||||
|
*/
|
||||||
|
export function isValidConfigKey(key: string): key is ConfigKey {
|
||||||
|
return VALID_CONFIG_KEYS.includes(key as ConfigKey);
|
||||||
|
}
|
||||||
23
packages/claw-market/src/lib/platform.ts
Normal file
23
packages/claw-market/src/lib/platform.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Detect the current platform
|
||||||
|
*/
|
||||||
|
export function detectPlatform(): string {
|
||||||
|
const platform = process.platform;
|
||||||
|
switch (platform) {
|
||||||
|
case "darwin":
|
||||||
|
return "darwin";
|
||||||
|
case "linux":
|
||||||
|
return "linux";
|
||||||
|
case "win32":
|
||||||
|
return "windows";
|
||||||
|
default:
|
||||||
|
return platform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get model identifier from environment or default
|
||||||
|
*/
|
||||||
|
export function detectModel(): string {
|
||||||
|
return process.env.CLAUDE_MODEL || process.env.ANTHROPIC_MODEL || "unknown";
|
||||||
|
}
|
||||||
66
packages/claw-market/src/lib/validate.ts
Normal file
66
packages/claw-market/src/lib/validate.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register input validation schema
|
||||||
|
*/
|
||||||
|
export const registerSchema = z.object({
|
||||||
|
name: z.string().min(1, "Name must be at least 1 character").max(100, "Name must be at most 100 characters"),
|
||||||
|
platform: z.string().optional(),
|
||||||
|
model: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Heartbeat input validation schema
|
||||||
|
*/
|
||||||
|
export const heartbeatSchema = z.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
platform: z.string().optional(),
|
||||||
|
model: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task input validation schema
|
||||||
|
*/
|
||||||
|
export const taskSchema = z.object({
|
||||||
|
summary: z.string().max(500, "Summary must be at most 500 characters"),
|
||||||
|
durationMs: z.number().positive("Duration must be a positive number"),
|
||||||
|
model: z.string().optional(),
|
||||||
|
toolsUsed: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RegisterInput = z.infer<typeof registerSchema>;
|
||||||
|
export type HeartbeatInput = z.infer<typeof heartbeatSchema>;
|
||||||
|
export type TaskInput = z.infer<typeof taskSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate register input
|
||||||
|
*/
|
||||||
|
export function validateRegister(input: unknown): { success: true; data: RegisterInput } | { success: false; error: string } {
|
||||||
|
const result = registerSchema.safeParse(input);
|
||||||
|
if (result.success) {
|
||||||
|
return { success: true, data: result.data };
|
||||||
|
}
|
||||||
|
return { success: false, error: result.error.errors[0]?.message || "Validation failed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate heartbeat input
|
||||||
|
*/
|
||||||
|
export function validateHeartbeat(input: unknown): { success: true; data: HeartbeatInput } | { success: false; error: string } {
|
||||||
|
const result = heartbeatSchema.safeParse(input);
|
||||||
|
if (result.success) {
|
||||||
|
return { success: true, data: result.data };
|
||||||
|
}
|
||||||
|
return { success: false, error: result.error.errors[0]?.message || "Validation failed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate task input
|
||||||
|
*/
|
||||||
|
export function validateTask(input: unknown): { success: true; data: TaskInput } | { success: false; error: string } {
|
||||||
|
const result = taskSchema.safeParse(input);
|
||||||
|
if (result.success) {
|
||||||
|
return { success: true, data: result.data };
|
||||||
|
}
|
||||||
|
return { success: false, error: result.error.errors[0]?.message || "Validation failed" };
|
||||||
|
}
|
||||||
21
packages/claw-market/tsconfig.json
Normal file
21
packages/claw-market/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
15
packages/claw-market/tsup.config.ts
Normal file
15
packages/claw-market/tsup.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from "tsup";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ["src/index.ts"],
|
||||||
|
format: ["esm"],
|
||||||
|
target: "node18",
|
||||||
|
clean: true,
|
||||||
|
dts: true,
|
||||||
|
sourcemap: true,
|
||||||
|
minify: false,
|
||||||
|
banner: {
|
||||||
|
js: "#!/usr/bin/env node",
|
||||||
|
},
|
||||||
|
external: ["commander", "zod"],
|
||||||
|
});
|
||||||
1402
pnpm-lock.yaml
generated
1402
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
packages:
|
||||||
|
- "packages/*"
|
||||||
@@ -14,8 +14,7 @@ requirements:
|
|||||||
writeFiles:
|
writeFiles:
|
||||||
- "~/.openclaw/config.json"
|
- "~/.openclaw/config.json"
|
||||||
binaries:
|
binaries:
|
||||||
- curl
|
- claw-market
|
||||||
- uname
|
|
||||||
env:
|
env:
|
||||||
- name: CLAUDE_MODEL
|
- name: CLAUDE_MODEL
|
||||||
required: false
|
required: false
|
||||||
@@ -24,7 +23,7 @@ hooks:
|
|||||||
SessionStart:
|
SessionStart:
|
||||||
- hooks:
|
- hooks:
|
||||||
- type: command
|
- type: command
|
||||||
command: "CONFIG=\"$HOME/.openclaw/config.json\"; [ -f \"$CONFIG\" ] && KEY=$(grep -o '\"apiKey\"[[:space:]]*:[[:space:]]*\"[^\"]*\"' \"$CONFIG\" | head -1 | sed 's/.*\"\\([^\"]*\\)\"$/\\1/') && [ -n \"$KEY\" ] && curl -s -o /dev/null --max-time 5 -X POST 'https://kymr.top/api/v1/heartbeat' -H 'Content-Type: application/json' -H \"Authorization: Bearer $KEY\" -d \"{\\\"platform\\\":\\\"$(uname -s | tr '[:upper:]' '[:lower:]')\\\",\\\"model\\\":\\\"${CLAUDE_MODEL:-unknown}\\\"}\" 2>/dev/null || true"
|
command: "claw-market heartbeat 2>/dev/null || true"
|
||||||
timeout: 10
|
timeout: 10
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -32,6 +31,14 @@ hooks:
|
|||||||
|
|
||||||
This skill lets you participate in the OpenClaw global claw activity heatmap. It reports **minimal, non-code** activity data to visualize worldwide AI agent usage. Note: as with any HTTPS request, the server can observe your IP address.
|
This skill lets you participate in the OpenClaw global claw activity heatmap. It reports **minimal, non-code** activity data to visualize worldwide AI agent usage. Note: as with any HTTPS request, the server can observe your IP address.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
This skill requires the `claw-market` CLI tool. Install it globally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @ricardweii/claw-market
|
||||||
|
```
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
- **Heartbeat**: Sent once when the skill is loaded (`SessionStart` hook). No other automated hooks are used — no `PostToolUse`, no `Stop` hooks.
|
- **Heartbeat**: Sent once when the skill is loaded (`SessionStart` hook). No other automated hooks are used — no `PostToolUse`, no `Stop` hooks.
|
||||||
@@ -57,13 +64,23 @@ This skill sends the following data to `https://kymr.top/` (the OpenClaw Market
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Config is stored at `~/.openclaw/config.json` (permissions `600`). No external scripts are deployed — the heartbeat is sent inline via the `SessionStart` hook command.
|
Config is stored at `~/.openclaw/config.json` (permissions `600`). Managed by the `claw-market` CLI tool.
|
||||||
|
|
||||||
## First-Time Setup (Requires User Consent)
|
## First-Time Setup (Requires User Consent)
|
||||||
|
|
||||||
If `~/.openclaw/config.json` does not exist, follow these steps:
|
If `~/.openclaw/config.json` does not exist, follow these steps:
|
||||||
|
|
||||||
### Step 1: Ask for consent
|
### Step 1: Check CLI availability
|
||||||
|
|
||||||
|
First, verify `claw-market` is installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
which claw-market || echo "NOT_INSTALLED"
|
||||||
|
```
|
||||||
|
|
||||||
|
If not installed, inform the user: "The openclaw-reporter skill requires the claw-market CLI. Install with: `npm install -g @ricardweii/claw-market`"
|
||||||
|
|
||||||
|
### Step 2: Ask for consent
|
||||||
|
|
||||||
Use the AskUserQuestion tool:
|
Use the AskUserQuestion tool:
|
||||||
|
|
||||||
@@ -72,7 +89,7 @@ Use the AskUserQuestion tool:
|
|||||||
|
|
||||||
**If the user selects "No, skip":** do NOT register and do NOT ask again in this session. Stop here.
|
**If the user selects "No, skip":** do NOT register and do NOT ask again in this session. Stop here.
|
||||||
|
|
||||||
### Step 2: Ask for a claw name
|
### Step 3: Ask for a claw name
|
||||||
|
|
||||||
If the user consented, use AskUserQuestion again:
|
If the user consented, use AskUserQuestion again:
|
||||||
|
|
||||||
@@ -82,56 +99,21 @@ If the user consented, use AskUserQuestion again:
|
|||||||
|
|
||||||
Use whatever the user provides as the claw name.
|
Use whatever the user provides as the claw name.
|
||||||
|
|
||||||
### Step 3: Register and save config
|
### Step 4: Register using CLI
|
||||||
|
|
||||||
After receiving the name (stored as `CLAW_NAME`), run the following:
|
After receiving the name, validate and register:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# --- OpenClaw Registration ---
|
# Validate claw name: only allow alphanumeric, hyphens, underscores (1-30 chars).
|
||||||
set -e
|
|
||||||
|
|
||||||
OPENCLAW_ENDPOINT="https://kymr.top"
|
|
||||||
CLAW_NAME="USER_CHOSEN_NAME_HERE"
|
CLAW_NAME="USER_CHOSEN_NAME_HERE"
|
||||||
|
|
||||||
# Validate claw name: only allow alphanumeric, hyphens, underscores (1-30 chars).
|
|
||||||
# This whitelist ensures $CLAW_NAME is safe for shell interpolation and JSON embedding.
|
|
||||||
if ! echo "$CLAW_NAME" | grep -qE '^[A-Za-z0-9_-]{1,30}$'; then
|
if ! echo "$CLAW_NAME" | grep -qE '^[A-Za-z0-9_-]{1,30}$'; then
|
||||||
echo "Error: Claw name must be 1-30 characters, alphanumeric/hyphens/underscores only."
|
echo "Error: Claw name must be 1-30 characters, alphanumeric/hyphens/underscores only."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p ~/.openclaw
|
# Register using CLI
|
||||||
|
claw-market register "$CLAW_NAME"
|
||||||
# Register with the server
|
|
||||||
# Safe: CLAW_NAME is validated above to contain only [A-Za-z0-9_-]
|
|
||||||
RESPONSE=$(curl -s -X POST "$OPENCLAW_ENDPOINT/api/v1/register" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"name\": \"$CLAW_NAME\",
|
|
||||||
\"platform\": \"$(uname -s | tr '[:upper:]' '[:lower:]')\",
|
|
||||||
\"model\": \"${CLAUDE_MODEL:-unknown}\"
|
|
||||||
}")
|
|
||||||
|
|
||||||
# Extract fields from JSON response using grep/sed (no python3 needed)
|
|
||||||
CLAW_ID=$(echo "$RESPONSE" | grep -o '"clawId"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
|
|
||||||
API_KEY=$(echo "$RESPONSE" | grep -o '"apiKey"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
|
|
||||||
|
|
||||||
if [ -z "$API_KEY" ]; then
|
|
||||||
echo "Error: Registration failed. Server response: $RESPONSE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Write config file
|
|
||||||
cat > ~/.openclaw/config.json << CFGEOF
|
|
||||||
{
|
|
||||||
"clawId": "$CLAW_ID",
|
|
||||||
"apiKey": "$API_KEY",
|
|
||||||
"name": "$CLAW_NAME"
|
|
||||||
}
|
|
||||||
CFGEOF
|
|
||||||
chmod 600 ~/.openclaw/config.json
|
|
||||||
|
|
||||||
echo "Registered as: $CLAW_NAME"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Task Reporting (Manual Only)
|
## Task Reporting (Manual Only)
|
||||||
@@ -141,15 +123,10 @@ Task reports are **never sent automatically**. Only send a task report when the
|
|||||||
When triggered, run:
|
When triggered, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
CONFIG="$HOME/.openclaw/config.json"
|
# Estimate task duration (ask user or use a default)
|
||||||
[ -f "$CONFIG" ] || { echo "OpenClaw not registered."; exit 0; }
|
DURATION_MS=60000 # Default 1 minute, adjust based on actual task time
|
||||||
KEY=$(grep -o '"apiKey"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG" | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
|
|
||||||
[ -z "$KEY" ] && { echo "No API key found."; exit 0; }
|
claw-market task "Completed a task" --duration "$DURATION_MS" || echo "Task report failed (non-blocking)."
|
||||||
curl -s -o /dev/null --max-time 5 -X POST 'https://kymr.top/api/v1/task' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-H "Authorization: Bearer $KEY" \
|
|
||||||
-d "{\"summary\":\"Completed a task\",\"model\":\"${CLAUDE_MODEL:-unknown}\"}" 2>/dev/null \
|
|
||||||
&& echo "Task reported." || echo "Task report failed (non-blocking)."
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Opt-Out / Unregister
|
## Opt-Out / Unregister
|
||||||
@@ -157,9 +134,7 @@ curl -s -o /dev/null --max-time 5 -X POST 'https://kymr.top/api/v1/task' \
|
|||||||
To stop all OpenClaw reporting:
|
To stop all OpenClaw reporting:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rm -f ~/.openclaw/config.json
|
claw-market config clear
|
||||||
rmdir ~/.openclaw 2>/dev/null
|
|
||||||
echo "OpenClaw fully uninstalled."
|
|
||||||
```
|
```
|
||||||
|
|
||||||
After deletion, no heartbeats or task reports will be sent. The skill will ask for consent again if re-enabled.
|
After deletion, no heartbeats or task reports will be sent. The skill will ask for consent again if re-enabled.
|
||||||
@@ -172,5 +147,19 @@ After deletion, no heartbeats or task reports will be sent. The skill will ask f
|
|||||||
4. **Task reports are manual only** — only send when the user explicitly mentions completing a task or asks to report
|
4. **Task reports are manual only** — only send when the user explicitly mentions completing a task or asks to report
|
||||||
5. If any OpenClaw request fails, continue normally — never interrupt the user's workflow
|
5. If any OpenClaw request fails, continue normally — never interrupt the user's workflow
|
||||||
6. Never include identifying information in task summaries
|
6. Never include identifying information in task summaries
|
||||||
7. **No external scripts**: all logic is inline or executed by the agent directly
|
7. **No system-level modifications**: never write to `~/.claude/settings.json` or any file outside `~/.openclaw/`
|
||||||
8. **No system-level modifications**: never write to `~/.claude/settings.json` or any file outside `~/.openclaw/`
|
|
||||||
|
## CLI Reference
|
||||||
|
|
||||||
|
The `claw-market` CLI provides these commands:
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `claw-market register <name>` | Register a new claw |
|
||||||
|
| `claw-market heartbeat` | Send a heartbeat |
|
||||||
|
| `claw-market task <summary> -d <ms>` | Report a completed task |
|
||||||
|
| `claw-market config show` | Show current configuration |
|
||||||
|
| `claw-market config path` | Show config file path |
|
||||||
|
| `claw-market config clear` | Delete configuration (unregister) |
|
||||||
|
|
||||||
|
Global options: `--lang <en\|zh>`, `--json`, `--endpoint <url>`
|
||||||
|
|||||||
Reference in New Issue
Block a user