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:
richarjiang
2026-03-15 13:59:57 +08:00
parent 48ac785290
commit 7db59c9290
24 changed files with 2968 additions and 63 deletions

View 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"));
}
});
}

View 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"));
}
});
}

View 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 }));
}
});
}

View 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"));
}
});
}

View 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}"
}
}

View 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>;

View 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}"
}
}

View 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();

View 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;
}

View 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);
}

View 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";
}

View 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" };
}