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:
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" };
|
||||
}
|
||||
Reference in New Issue
Block a user