diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index c8386c0..4aef32a 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -50,23 +50,18 @@ export default function HomePage() { {/* Section 1: Main Map View + Dashboard */} -
-
- -
- -
- {/* Left Panel */} -
+
+
+ {/* Left Panel โ€” Stats */} +
- {/* Center - Map/Globe + Timeline */} + {/* Center โ€” Map/Globe (hero of the page) */}
- {/* View Switcher + Map Container */} -
+
{/* View Mode Toggle */}
- {/* Map View */} {viewMode === "2d" && ( )} - - {/* Globe View */} {viewMode === "3d" && ( )} @@ -109,8 +101,9 @@ export default function HomePage() {
- {/* Right Panel */} + {/* Right Panel โ€” Register + Feed */}
+
diff --git a/app/api/v1/device/name/route.ts b/app/api/v1/device/name/route.ts new file mode 100644 index 0000000..8867ef4 --- /dev/null +++ b/app/api/v1/device/name/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from "next/server"; +import { eq } from "drizzle-orm"; +import { db } from "@/lib/db"; +import { claws } from "@/lib/db/schema"; +import { authenticateRequest } from "@/lib/auth/request"; +import { updateNameSchema } from "@/lib/validators/schemas"; + +export async function PUT(req: NextRequest) { + try { + const auth = await authenticateRequest(req); + if (auth instanceof NextResponse) { + return auth; + } + const { claw } = auth; + + const body = await req.json(); + const parsed = updateNameSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: "Validation failed", details: parsed.error.flatten() }, + { status: 400 } + ); + } + + const { name } = parsed.data; + + await db + .update(claws) + .set({ name, updatedAt: new Date() }) + .where(eq(claws.id, claw.id)); + + return NextResponse.json({ ok: true, name }); + } catch (error) { + console.error("Update name error:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/app/api/v1/device/register/route.ts b/app/api/v1/device/register/route.ts new file mode 100644 index 0000000..b3c1796 --- /dev/null +++ b/app/api/v1/device/register/route.ts @@ -0,0 +1,150 @@ +import { NextRequest, NextResponse } from "next/server"; +import { nanoid } from "nanoid"; +import { eq } from "drizzle-orm"; +import { db } from "@/lib/db"; +import { claws } from "@/lib/db/schema"; +import { redis } from "@/lib/redis"; +import { generateApiKey } from "@/lib/auth/api-key"; +import { getGeoLocation } from "@/lib/geo/ip-location"; +import { applyDeterministicOffset } from "@/lib/geo/offset"; +import { deviceRegisterSchema } from "@/lib/validators/schemas"; + +function getClientIp(req: NextRequest): string { + const forwarded = req.headers.get("x-forwarded-for"); + if (forwarded) return forwarded.split(",")[0].trim(); + const realIp = req.headers.get("x-real-ip"); + if (realIp) return realIp.trim(); + return "127.0.0.1"; +} + +const RATE_LIMIT_KEY_PREFIX = "ratelimit:device:"; +const RATE_LIMIT_MAX = 10; +const RATE_LIMIT_WINDOW = 3600; // 1 hour + +export async function POST(req: NextRequest) { + try { + const clientIp = getClientIp(req); + + // Rate limit check + const rateLimitKey = `${RATE_LIMIT_KEY_PREFIX}${clientIp}`; + const currentCount = await redis.incr(rateLimitKey); + if (currentCount === 1) { + await redis.expire(rateLimitKey, RATE_LIMIT_WINDOW); + } + if (currentCount > RATE_LIMIT_MAX) { + return NextResponse.json( + { error: "Too many requests. Try again later." }, + { status: 429 } + ); + } + + const body = await req.json(); + const parsed = deviceRegisterSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: "Validation failed", details: parsed.error.flatten() }, + { status: 400 } + ); + } + + const { deviceId, name, platform } = parsed.data; + + // Check if device already registered + const existing = await db + .select() + .from(claws) + .where(eq(claws.deviceId, deviceId)) + .limit(1); + + if (existing.length > 0) { + return NextResponse.json({ + clawId: existing[0].id, + apiKey: existing[0].apiKey, + name: existing[0].name, + isNew: false, + }); + } + + // Create new claw + const clawId = nanoid(21); + const apiKey = generateApiKey(); + const geo = await getGeoLocation(clientIp); + const now = new Date(); + + let finalLat: number | null = null; + let finalLng: number | null = null; + if (geo) { + const offset = applyDeterministicOffset( + geo.latitude, + geo.longitude, + clawId + ); + finalLat = offset.lat; + finalLng = offset.lng; + } + + // Auto-generate name if not provided + const clawName = name || generateClawName(); + + await db.insert(claws).values({ + id: clawId, + apiKey, + deviceId, + name: clawName, + platform: platform ? platform.slice(0, 20) : null, + ip: clientIp, + latitude: finalLat !== null ? String(finalLat) : null, + longitude: finalLng !== null ? String(finalLng) : null, + city: geo?.city ?? null, + country: geo?.country ?? null, + countryCode: geo?.countryCode ?? null, + region: geo?.region ?? null, + lastHeartbeat: now, + totalTasks: 0, + createdAt: now, + updatedAt: now, + }); + + return NextResponse.json({ + clawId, + apiKey, + name: clawName, + isNew: true, + }); + } catch (error) { + console.error("Device register error:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +const ADJECTIVES = [ + "Swift", + "Brave", + "Cool", + "Dark", + "Fire", + "Ice", + "Storm", + "Shadow", + "Cyber", + "Neon", + "Pixel", + "Turbo", + "Mega", + "Ultra", + "Nova", + "Star", + "Flash", + "Thunder", + "Iron", + "Cosmic", +]; + +function generateClawName(): string { + const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]; + const num = Math.floor(Math.random() * 100); + return `${adj}Claw-${num}`; +} diff --git a/components/dashboard/claw-feed.tsx b/components/dashboard/claw-feed.tsx index b232f33..7b75e4e 100644 --- a/components/dashboard/claw-feed.tsx +++ b/components/dashboard/claw-feed.tsx @@ -181,6 +181,7 @@ export function ClawFeed() { {formatDuration(item.durationMs)} )} + {new Date(item.timestamp).toLocaleDateString(locale, { month: "2-digit", day: "2-digit" })}{" "} {new Date(item.timestamp).toLocaleTimeString(locale)}
diff --git a/components/layout/hero.tsx b/components/layout/hero.tsx index 6438e91..e82c076 100644 --- a/components/layout/hero.tsx +++ b/components/layout/hero.tsx @@ -8,36 +8,36 @@ export function Hero() { const t = useTranslations("hero"); return ( -
+
{/* Badge */} - + {t("badge")} {/* Main Title */} -

+

{t("title")}

{/* Subtitle */} -

+

{t("subtitle")}

{/* CTA Buttons */} -
+
- + {t("cta")} - + {t("secondary")} diff --git a/components/layout/install-banner.tsx b/components/layout/install-banner.tsx index 1f30b64..9d8849b 100644 --- a/components/layout/install-banner.tsx +++ b/components/layout/install-banner.tsx @@ -1,124 +1,329 @@ "use client"; -import { useState } from "react"; -import { Check, Copy, MessageSquare, Bot } from "lucide-react"; +import { useState, useCallback } from "react"; +import { + Check, + Copy, + MessageSquare, + Terminal, + Pencil, + Eye, + EyeOff, + Bot, + Loader2, +} from "lucide-react"; import { useTranslations } from "next-intl"; +import { useDeviceToken } from "@/hooks/use-device-token"; +import { cn } from "@/lib/utils"; + +type Tab = "openclaw" | "terminal"; export function InstallBanner() { const t = useTranslations("installBanner"); const tGuide = useTranslations("skillGuide"); + const { token, name, isLoading, error, updateName } = useDeviceToken(); + + const [activeTab, setActiveTab] = useState("terminal"); const [copied, setCopied] = useState(false); + const [isEditingName, setIsEditingName] = useState(false); + const [editNameValue, setEditNameValue] = useState(""); + const [nameStatus, setNameStatus] = useState< + "idle" | "saving" | "success" | "error" + >("idle"); + const [showToken, setShowToken] = useState(false); - const prompt = t("prompt"); + const maskedToken = token ? `${token.slice(0, 6)}...` : ""; - const handleCopy = async () => { + const openclawPrompt = token ? t("openclawPrompt", { token }) : ""; + const terminalCommand = token ? t("terminalCommand", { token }) : ""; + + const handleCopy = useCallback(async (text: string) => { try { - await navigator.clipboard.writeText(prompt); + await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { - // fallback: select the text + // fallback } - }; + }, []); + + const handleEditName = useCallback(() => { + setEditNameValue(name || ""); + setIsEditingName(true); + setNameStatus("idle"); + }, [name]); + + const handleSaveName = useCallback(async () => { + if (!editNameValue.trim()) return; + setNameStatus("saving"); + const ok = await updateName(editNameValue.trim()); + if (ok) { + setNameStatus("success"); + setIsEditingName(false); + setTimeout(() => setNameStatus("idle"), 2000); + } else { + setNameStatus("error"); + } + }, [editNameValue, updateName]); + + const handleCancelEdit = useCallback(() => { + setIsEditingName(false); + setNameStatus("idle"); + }, []); + + // Loading state + if (isLoading) { + return ( +
+
+ + + {t("generatingToken")} + +
+
+ ); + } + + // Error state + if (error || !token) { + return ( +
+
+ {t("tokenError")} +
+
+ ); + } return (
-
- {/* Header */} +
+ {/* Compact header */}
-
-
- ๐Ÿฆž -
-
-

- {t("title")} -

-

- {t("subtitle")} -

-
-
-
+ + {/* Claw name row */} +
+
+ + {t("yourClaw")}: + + {isEditingName ? ( +
+ setEditNameValue(e.target.value)} + className="w-20 rounded border border-[var(--accent-cyan)]/30 bg-[var(--bg-primary)] px-1.5 py-0.5 text-[11px] text-[var(--text-primary)] outline-none focus:border-[var(--accent-cyan)]" + maxLength={30} + autoFocus + onKeyDown={(e) => { + if (e.key === "Enter") handleSaveName(); + if (e.key === "Escape") handleCancelEdit(); + }} + /> + + +
+ ) : ( +
+ + {name} + + {nameStatus === "success" && ( + + )} + +
+ )} +
+ + {/* Token mini display */} +
- {/* Human prompt card */} - + {/* Compact tabs */} +
+ + +
- {/* AI-crawlable installation guide (collapsed for humans, fully in DOM for crawlers) */} + {/* Tab content */} + {activeTab === "openclaw" && ( +
+ + +
+ )} + + {activeTab === "terminal" && ( +
+
+
+ + $ + + + {terminalCommand} + +
+
+ +
+ )} + + {/* AI-crawlable guide (collapsed) */}
- - + + {tGuide("summaryLabel")} - -
+
-

{tGuide("whatTitle")}

+

+ {tGuide("whatTitle")} +

{tGuide("whatDescription")}

-
-

{tGuide("installTitle")}

-
    +

    + {tGuide("installTitle")} +

    +
    1. {tGuide("step1")}
    2. {tGuide("step2")}
    3. {tGuide("step3")}
    4. {tGuide("step4")}
-
-

{tGuide("commandTitle")}

- - clawhub install openclaw-reporter +

+ {tGuide("commandTitle")} +

+ + bash <(curl -fsSL https://kymr.top/install.sh) --token + YOUR_TOKEN
-
-

{tGuide("dataTitle")}

+

+ {tGuide("dataTitle")} +

  • {tGuide("data1")}
  • {tGuide("data2")}
  • {tGuide("data3")}
  • {tGuide("data4")}
-

{tGuide("dataNever")}

+

{tGuide("dataNever")}

-
-

{tGuide("permissionsTitle")}

+

+ {tGuide("permissionsTitle")} +

  • {tGuide("perm1")}
  • {tGuide("perm2")}
  • diff --git a/hooks/use-device-token.ts b/hooks/use-device-token.ts new file mode 100644 index 0000000..442984d --- /dev/null +++ b/hooks/use-device-token.ts @@ -0,0 +1,171 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +interface DeviceTokenData { + clawId: string; + apiKey: string; + name: string; + deviceId: string; +} + +interface UseDeviceTokenReturn { + token: string | null; + clawId: string | null; + name: string | null; + isLoading: boolean; + error: string | null; + updateName: (newName: string) => Promise; +} + +const STORAGE_KEY = "openclaw_device"; + +function generateUUID(): string { + if (typeof crypto !== "undefined" && crypto.randomUUID) { + return crypto.randomUUID(); + } + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +function getDeviceInfo(): { + platform: string; + browser: string; + screen: string; + language: string; +} { + if (typeof navigator === "undefined") { + return { platform: "unknown", browser: "unknown", screen: "unknown", language: "en" }; + } + + const ua = navigator.userAgent; + let browser = "Unknown"; + if (ua.includes("Chrome") && !ua.includes("Edg")) browser = "Chrome"; + else if (ua.includes("Firefox")) browser = "Firefox"; + else if (ua.includes("Safari") && !ua.includes("Chrome")) browser = "Safari"; + else if (ua.includes("Edg")) browser = "Edge"; + + return { + platform: navigator.platform || "unknown", + browser, + screen: + typeof screen !== "undefined" + ? `${screen.width}x${screen.height}` + : "unknown", + language: navigator.language || "en", + }; +} + +function getCachedData(): DeviceTokenData | null { + if (typeof localStorage === "undefined") return null; + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + return JSON.parse(raw) as DeviceTokenData; + } catch { + return null; + } +} + +function setCachedData(data: DeviceTokenData): void { + if (typeof localStorage === "undefined") return; + localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); +} + +function updateCachedName(newName: string): void { + const data = getCachedData(); + if (data) { + setCachedData({ ...data, name: newName }); + } +} + +export function useDeviceToken(): UseDeviceTokenReturn { + const [token, setToken] = useState(null); + const [clawId, setClawId] = useState(null); + const [name, setName] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const init = async () => { + // Check cache first + const cached = getCachedData(); + if (cached) { + setToken(cached.apiKey); + setClawId(cached.clawId); + setName(cached.name); + setIsLoading(false); + return; + } + + // Register new device + try { + const deviceId = generateUUID(); + const deviceInfo = getDeviceInfo(); + + const res = await fetch("/api/v1/device/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + deviceId, + ...deviceInfo, + }), + }); + + if (!res.ok) { + throw new Error(`Registration failed: ${res.status}`); + } + + const data = await res.json(); + const tokenData: DeviceTokenData = { + clawId: data.clawId, + apiKey: data.apiKey, + name: data.name, + deviceId, + }; + + setCachedData(tokenData); + setToken(data.apiKey); + setClawId(data.clawId); + setName(data.name); + } catch (err) { + setError(err instanceof Error ? err.message : "Registration failed"); + } finally { + setIsLoading(false); + } + }; + + init(); + }, []); + + const updateName = useCallback( + async (newName: string): Promise => { + if (!token) return false; + + try { + const res = await fetch("/api/v1/device/name", { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ name: newName }), + }); + + if (!res.ok) return false; + + setName(newName); + updateCachedName(newName); + return true; + } catch { + return false; + } + }, + [token] + ); + + return { token, clawId, name, isLoading, error, updateName }; +} diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 7531513..337e010 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -11,24 +11,29 @@ import { } from "drizzle-orm/mysql-core"; import { sql } from "drizzle-orm"; -export const claws = mysqlTable("claws", { - id: varchar("id", { length: 21 }).primaryKey(), - apiKey: varchar("api_key", { length: 64 }).notNull().unique(), - name: varchar("name", { length: 100 }).notNull(), - platform: varchar("platform", { length: 20 }), - model: varchar("model", { length: 50 }), - ip: varchar("ip", { length: 45 }), - latitude: decimal("latitude", { precision: 10, scale: 7 }), - longitude: decimal("longitude", { precision: 10, scale: 7 }), - city: varchar("city", { length: 100 }), - country: varchar("country", { length: 100 }), - countryCode: varchar("country_code", { length: 5 }), - region: varchar("region", { length: 50 }), - lastHeartbeat: datetime("last_heartbeat"), - totalTasks: int("total_tasks").default(0), - createdAt: datetime("created_at").default(sql`NOW()`), - updatedAt: datetime("updated_at").default(sql`NOW()`), -}); +export const claws = mysqlTable( + "claws", + { + id: varchar("id", { length: 21 }).primaryKey(), + apiKey: varchar("api_key", { length: 64 }).notNull().unique(), + deviceId: varchar("device_id", { length: 64 }), + name: varchar("name", { length: 100 }).notNull(), + platform: varchar("platform", { length: 20 }), + model: varchar("model", { length: 50 }), + ip: varchar("ip", { length: 45 }), + latitude: decimal("latitude", { precision: 10, scale: 7 }), + longitude: decimal("longitude", { precision: 10, scale: 7 }), + city: varchar("city", { length: 100 }), + country: varchar("country", { length: 100 }), + countryCode: varchar("country_code", { length: 5 }), + region: varchar("region", { length: 50 }), + lastHeartbeat: datetime("last_heartbeat"), + totalTasks: int("total_tasks").default(0), + createdAt: datetime("created_at").default(sql`NOW()`), + updatedAt: datetime("updated_at").default(sql`NOW()`), + }, + (table) => [uniqueIndex("claws_device_id_unq").on(table.deviceId)] +); export const heartbeats = mysqlTable( "heartbeats", diff --git a/lib/validators/schemas.ts b/lib/validators/schemas.ts index 7676d49..dbae5ee 100644 --- a/lib/validators/schemas.ts +++ b/lib/validators/schemas.ts @@ -27,7 +27,26 @@ export const tokenSchema = z.object({ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), }); +export const deviceRegisterSchema = z.object({ + deviceId: z.string().min(1).max(64), + name: z.string().min(1).max(100).optional(), + platform: z.string().max(50).optional(), + browser: z.string().max(100).optional(), + screen: z.string().max(20).optional(), + language: z.string().max(10).optional(), +}); + +export const updateNameSchema = z.object({ + name: z + .string() + .min(1) + .max(30) + .regex(/^[A-Za-z0-9_-]+$/, "Only alphanumeric, hyphens, and underscores"), +}); + export type RegisterInput = z.infer; export type HeartbeatInput = z.infer; export type TaskInput = z.infer; export type TokenInput = z.infer; +export type DeviceRegisterInput = z.infer; +export type UpdateNameInput = z.infer; diff --git a/messages/en.json b/messages/en.json index a936283..9adcfc1 100644 --- a/messages/en.json +++ b/messages/en.json @@ -17,12 +17,29 @@ "live": "Live" }, "installBanner": { - "title": "Join the Heatmap", - "subtitle": "Copy and paste this into Claude Code to get started", - "copyTooltip": "Copy prompt to clipboard", + "title": "Register Your Agent", + "subtitle": "Get your agent on the global map in seconds", + "tabOpenClaw": "OpenClaw", + "tabTerminal": "Terminal", + "openclawPrompt": "Help me join the OpenClaw global heatmap! Run this install command in the terminal: bash <(curl -fsSL https://kymr.top/install.sh) --token {token}", + "terminalCommand": "bash <(curl -fsSL https://kymr.top/install.sh) --token {token}", "copyButton": "Copy", "copied": "Copied!", - "prompt": "Help me join the OpenClaw global heatmap! First, search for the \"openclaw-reporter\" skill on ClawHub and install it. After installation, help me pick a cool name for my claw (like CoolClaw, NightCrawler, etc.) and guide me through the registration and permission setup." + "copyPrompt": "Copy prompt", + "copyCommand": "Copy command", + "openclawHint": "Paste this prompt into your OpenClaw session", + "terminalHint": "Run this command in your terminal", + "yourClaw": "Your Claw", + "editName": "Edit", + "saveName": "Save", + "cancelEdit": "Cancel", + "nameUpdated": "Name updated!", + "nameError": "Failed to update name", + "tokenLabel": "Token", + "showToken": "Show", + "hideToken": "Hide", + "generatingToken": "Generating your token...", + "tokenError": "Failed to generate token. Please refresh." }, "stats": { "onlineNow": "Online Now", @@ -90,26 +107,26 @@ "close": "Close" }, "skillGuide": { - "ariaLabel": "OpenClaw Reporter Skill Installation Guide for AI Agents", + "ariaLabel": "OpenClaw Installation Guide for AI Agents", "summaryLabel": "AI Agent Installation Guide (click to expand)", - "whatTitle": "What is OpenClaw Reporter?", - "whatDescription": "openclaw-reporter is a Claude Code skill that lets you participate in the OpenClaw global heatmap at https://kymr.top/. It sends anonymous, non-identifying heartbeats (platform + model only) and generic task summaries. No file paths, code, tool names, or project-specific data is ever sent.", - "installTitle": "Quick Install (4 Steps)", - "step1": "Run \"clawhub install openclaw-reporter\" in your terminal, or use \"/clawhub install openclaw-reporter\" inside a Claude Code session.", - "step2": "The skill will ask for your consent before registering โ€” agree to allow anonymous heartbeats and task summaries to be sent to https://kymr.top/.", - "step3": "Pick a name for your claw (your display name on the global heatmap, e.g. CoolClaw, NightCrawler, CodeClaw).", - "step4": "Done! Your claw will appear on the heatmap. Heartbeats are sent automatically via hooks (rate-limited to once per 3 minutes), and a task summary is sent when your session ends.", + "whatTitle": "What is OpenClaw?", + "whatDescription": "OpenClaw lets you participate in the global claw activity heatmap at https://kymr.top/. A lightweight install script sets up automated heartbeats and token usage reporting. No file paths, code, or project-specific data is ever sent.", + "installTitle": "Quick Install", + "step1": "Visit https://kymr.top/ to get your unique token.", + "step2": "Run: bash <(curl -fsSL https://kymr.top/install.sh) --token YOUR_TOKEN", + "step3": "The script installs the CLI, sets up heartbeat monitoring (every 5 min), and token reporting (every 2 hours).", + "step4": "Done! Your claw appears on the heatmap automatically.", "commandTitle": "Install Command", "dataTitle": "Data Collected", - "data1": "Claw Name โ€” your chosen display name (e.g. CoolClaw42)", + "data1": "Claw Name โ€” your display name on the heatmap", "data2": "Platform โ€” OS type (darwin, linux, win32)", - "data3": "Model โ€” Claude model identifier (e.g. claude-sonnet-4-6)", - "data4": "Task summary โ€” generic activity indicator (\"Completed a task\")", + "data3": "Model โ€” AI model identifier", + "data4": "Token usage โ€” daily input/output token counts", "dataNever": "Never sent: usernames, file paths, code, project names, tool names, secrets, or any identifying information.", - "permissionsTitle": "Permissions Required", - "perm1": "Network access โ€” to send heartbeats and task reports to https://kymr.top/", - "perm2": "Write ~/.openclaw/ โ€” to store config (API key) and hook scripts locally", - "perm3": "Binaries: curl, python3, uname โ€” used by hook scripts" + "permissionsTitle": "What Gets Installed", + "perm1": "claw-market CLI โ€” lightweight command-line tool", + "perm2": "~/.openclaw/ โ€” config file and helper scripts", + "perm3": "Cron jobs โ€” heartbeat check (5 min) and token report (2 hr)" }, "languageSwitcher": { "en": "EN", diff --git a/messages/zh.json b/messages/zh.json index b2ceb82..39e870c 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -17,12 +17,29 @@ "live": "ๅฎžๆ—ถ" }, "installBanner": { - "title": "ๅŠ ๅ…ฅ็ƒญๅŠ›ๅ›พ", - "subtitle": "ๅคๅˆถไธ‹้ข่ฟ™ๆฎต่ฏ๏ผŒ็ฒ˜่ดดๅˆฐ Claude Code ๅฏน่ฏไธญๅณๅฏๅผ€ๅง‹", - "copyTooltip": "ๅคๅˆถๅˆฐๅ‰ช่ดดๆฟ", + "title": "ๆณจๅ†Œไฝ ็š„ Agent", + "subtitle": "ๅ‡ ็ง’ๅ†…่ฎฉไฝ ็š„ Agent ๅ‡บ็Žฐๅœจๅ…จ็ƒๅœฐๅ›พไธŠ", + "tabOpenClaw": "OpenClaw", + "tabTerminal": "็ปˆ็ซฏๅฎ‰่ฃ…", + "openclawPrompt": "ๅธฎๆˆ‘ๅŠ ๅ…ฅ OpenClaw ๅ…จ็ƒ้พ™่™พ็ƒญๅŠ›ๅ›พ๏ผๅœจ็ปˆ็ซฏๆ‰ง่กŒ่ฟ™ไธชๅฎ‰่ฃ…ๅ‘ฝไปค๏ผšbash <(curl -fsSL https://kymr.top/install.sh) --token {token}", + "terminalCommand": "bash <(curl -fsSL https://kymr.top/install.sh) --token {token}", "copyButton": "ๅคๅˆถ", "copied": "ๅทฒๅคๅˆถ๏ผ", - "prompt": "ๅธฎๆˆ‘ๅŠ ๅ…ฅ OpenClaw ๅ…จ็ƒ้พ™่™พ็ƒญๅŠ›ๅ›พ๏ผ้ฆ–ๅ…ˆ๏ผŒๅœจ ClawHub ไธŠๆœ็ดขๅนถๅฎ‰่ฃ… \"openclaw-reporter\" ๆŠ€่ƒฝใ€‚ๅฎ‰่ฃ…ๅฎŒๆˆๅŽ๏ผŒๅธฎๆˆ‘็ป™ๆˆ‘็š„ๅฐ้พ™่™พ่ตทไธ€ไธช้…ท็‚ซ็š„ๅๅญ—๏ผˆๆฏ”ๅฆ‚ ้…ท้พ™่™พใ€ๆš—ๅคœ้พ™่™พใ€ไปฃ็ ้พ™่™พ ็ญ‰๏ผ‰๏ผŒ็„ถๅŽๅผ•ๅฏผๆˆ‘ๅฎŒๆˆๆณจๅ†Œๅ’Œๆƒ้™ๆŽˆๆƒใ€‚" + "copyPrompt": "ๅคๅˆถๆ็คบ่ฏ", + "copyCommand": "ๅคๅˆถๅ‘ฝไปค", + "openclawHint": "ๅฐ†ๆญคๆ็คบ่ฏ็ฒ˜่ดดๅˆฐไฝ ็š„ OpenClaw ไผš่ฏไธญ", + "terminalHint": "ๅœจ็ปˆ็ซฏไธญ่ฟ่กŒๆญคๅ‘ฝไปค", + "yourClaw": "ไฝ ็š„้พ™่™พ", + "editName": "็ผ–่พ‘", + "saveName": "ไฟๅญ˜", + "cancelEdit": "ๅ–ๆถˆ", + "nameUpdated": "ๅ็งฐๅทฒๆ›ดๆ–ฐ๏ผ", + "nameError": "ๆ›ดๆ–ฐๅ็งฐๅคฑ่ดฅ", + "tokenLabel": "ไปค็‰Œ", + "showToken": "ๆ˜พ็คบ", + "hideToken": "้š่—", + "generatingToken": "ๆญฃๅœจ็”Ÿๆˆไฝ ็š„ไปค็‰Œ...", + "tokenError": "็”Ÿๆˆไปค็‰Œๅคฑ่ดฅ๏ผŒ่ฏทๅˆทๆ–ฐ้กต้ขใ€‚" }, "stats": { "onlineNow": "ๅฝ“ๅ‰ๅœจ็บฟ", @@ -90,26 +107,26 @@ "close": "ๅ…ณ้—ญ" }, "skillGuide": { - "ariaLabel": "OpenClaw Reporter ๆŠ€่ƒฝๅฎ‰่ฃ…ๆŒ‡ๅ—๏ผˆไพ› AI ไปฃ็†้˜…่ฏป๏ผ‰", + "ariaLabel": "OpenClaw ๅฎ‰่ฃ…ๆŒ‡ๅ—๏ผˆไพ› AI ไปฃ็†้˜…่ฏป๏ผ‰", "summaryLabel": "AI ไปฃ็†ๅฎ‰่ฃ…ๆŒ‡ๅ—๏ผˆ็‚นๅ‡ปๅฑ•ๅผ€๏ผ‰", - "whatTitle": "ไป€ไนˆๆ˜ฏ OpenClaw Reporter๏ผŸ", - "whatDescription": "openclaw-reporter ๆ˜ฏไธ€ไธช Claude Code ๆŠ€่ƒฝ๏ผŒ่ฎฉไฝ ๅ‚ไธŽ OpenClaw ๅ…จ็ƒ็ƒญๅŠ›ๅ›พ๏ผˆhttps://kymr.top/๏ผ‰ใ€‚ๅฎƒๅชๅ‘้€ๅŒฟๅ็š„ใ€้ž่บซไปฝ่ฏ†ๅˆซ็š„ๅฟƒ่ทณ๏ผˆๆ“ไฝœ็ณป็ปŸ + ๆจกๅž‹ไฟกๆฏ๏ผ‰ๅ’Œ้€š็”จไปปๅŠกๆ‘˜่ฆใ€‚็ปไธๅ‘้€ๆ–‡ไปถ่ทฏๅพ„ใ€ไปฃ็ ใ€ๅทฅๅ…ทๅ็งฐๆˆ–ไปปไฝ•้กน็›ฎ็›ธๅ…ณๆ•ฐๆฎใ€‚", - "installTitle": "ๅฟซ้€Ÿๅฎ‰่ฃ…๏ผˆ4 ๆญฅ๏ผ‰", - "step1": "ๅœจ็ปˆ็ซฏ่ฟ่กŒ \"clawhub install openclaw-reporter\"๏ผŒๆˆ–ๅœจ Claude Code ๅฏน่ฏไธญ่พ“ๅ…ฅ \"/clawhub install openclaw-reporter\"ใ€‚", - "step2": "ๆŠ€่ƒฝไผšๅœจๆณจๅ†Œๅ‰ๅพๆฑ‚ไฝ ็š„ๅŒๆ„โ€”โ€”ๅŒๆ„ๅ…่ฎธๅ‘ https://kymr.top/ ๅ‘้€ๅŒฟๅๅฟƒ่ทณๅ’ŒไปปๅŠกๆ‘˜่ฆใ€‚", - "step3": "็ป™ไฝ ็š„ๅฐ้พ™่™พ่ตทไธ€ไธชๅๅญ—๏ผˆไฝ ๅœจๅ…จ็ƒ็ƒญๅŠ›ๅ›พไธŠ็š„ๆ˜พ็คบๅ็งฐ๏ผŒๆฏ”ๅฆ‚ ้…ท้พ™่™พใ€ๆš—ๅคœ้พ™่™พใ€ไปฃ็ ้พ™่™พ๏ผ‰ใ€‚", - "step4": "ๅฎŒๆˆ๏ผไฝ ็š„ๅฐ้พ™่™พไผšๅ‡บ็Žฐๅœจ็ƒญๅŠ›ๅ›พไธŠใ€‚ๅฟƒ่ทณ้€š่ฟ‡ hooks ่‡ชๅŠจๅ‘้€๏ผˆ้™ๅˆถไธบๆฏ 3 ๅˆ†้’Ÿๆœ€ๅคšไธ€ๆฌก๏ผ‰๏ผŒไผš่ฏ็ป“ๆŸๆ—ถไผšๅ‘้€ไธ€ๆกไปปๅŠกๆ‘˜่ฆใ€‚", + "whatTitle": "ไป€ไนˆๆ˜ฏ OpenClaw๏ผŸ", + "whatDescription": "OpenClaw ่ฎฉไฝ ๅ‚ไธŽๅ…จ็ƒ้พ™่™พๆดปๅŠจ็ƒญๅŠ›ๅ›พ๏ผˆhttps://kymr.top/๏ผ‰ใ€‚่ฝป้‡ๅฎ‰่ฃ…่„šๆœฌ่‡ชๅŠจ่ฎพ็ฝฎๅฟƒ่ทณ็›‘ๆŽงๅ’Œ token ็”จ้‡ไธŠๆŠฅใ€‚็ปไธๅ‘้€ๆ–‡ไปถ่ทฏๅพ„ใ€ไปฃ็ ๆˆ–ไปปไฝ•้กน็›ฎ็›ธๅ…ณๆ•ฐๆฎใ€‚", + "installTitle": "ๅฟซ้€Ÿๅฎ‰่ฃ…", + "step1": "่ฎฟ้—ฎ https://kymr.top/ ่Žทๅ–ไฝ ็š„ไธ“ๅฑžไปค็‰Œใ€‚", + "step2": "่ฟ่กŒ๏ผšbash <(curl -fsSL https://kymr.top/install.sh) --token ไฝ ็š„ไปค็‰Œ", + "step3": "่„šๆœฌไผšๅฎ‰่ฃ… CLI ๅทฅๅ…ท๏ผŒ่ฎพ็ฝฎๅฟƒ่ทณ็›‘ๆŽง๏ผˆๆฏ 5 ๅˆ†้’Ÿ๏ผ‰ๅ’Œ token ไธŠๆŠฅ๏ผˆๆฏ 2 ๅฐๆ—ถ๏ผ‰ใ€‚", + "step4": "ๅฎŒๆˆ๏ผไฝ ็š„้พ™่™พไผš่‡ชๅŠจๅ‡บ็Žฐๅœจ็ƒญๅŠ›ๅ›พไธŠใ€‚", "commandTitle": "ๅฎ‰่ฃ…ๅ‘ฝไปค", "dataTitle": "ๆ”ถ้›†็š„ๆ•ฐๆฎ", - "data1": "้พ™่™พๅ็งฐโ€”โ€”ไฝ ้€‰ๆ‹ฉ็š„ๆ˜พ็คบๅ็งฐ๏ผˆๅฆ‚ ้…ท้พ™่™พ42๏ผ‰", + "data1": "้พ™่™พๅ็งฐโ€”โ€”ไฝ ๅœจ็ƒญๅŠ›ๅ›พไธŠ็š„ๆ˜พ็คบๅ็งฐ", "data2": "ๆ“ไฝœ็ณป็ปŸโ€”โ€”็ณป็ปŸ็ฑปๅž‹๏ผˆdarwinใ€linuxใ€win32๏ผ‰", - "data3": "ๆจกๅž‹โ€”โ€”Claude ๆจกๅž‹ๆ ‡่ฏ†็ฌฆ๏ผˆๅฆ‚ claude-sonnet-4-6๏ผ‰", - "data4": "ไปปๅŠกๆ‘˜่ฆโ€”โ€”้€š็”จๆดปๅŠจๆŒ‡ๆ ‡๏ผˆ\"Completed a task\"๏ผ‰", + "data3": "ๆจกๅž‹โ€”โ€”AI ๆจกๅž‹ๆ ‡่ฏ†็ฌฆ", + "data4": "Token ็”จ้‡โ€”โ€”ๆฏๆ—ฅ่พ“ๅ…ฅ/่พ“ๅ‡บ token ๆ•ฐ้‡", "dataNever": "็ปไธๅ‘้€๏ผš็”จๆˆทๅใ€ๆ–‡ไปถ่ทฏๅพ„ใ€ไปฃ็ ใ€้กน็›ฎๅ็งฐใ€ๅทฅๅ…ทๅ็งฐใ€ๅฏ†้’ฅๆˆ–ไปปไฝ•่บซไปฝ่ฏ†ๅˆซไฟกๆฏใ€‚", - "permissionsTitle": "ๆ‰€้œ€ๆƒ้™", - "perm1": "็ฝ‘็ปœ่ฎฟ้—ฎโ€”โ€”ๅ‘ https://kymr.top/ ๅ‘้€ๅฟƒ่ทณๅ’ŒไปปๅŠกๆŠฅๅ‘Š", - "perm2": "ๅ†™ๅ…ฅ ~/.openclaw/โ€”โ€”ๅœจๆœฌๅœฐๅญ˜ๅ‚จ้…็ฝฎ๏ผˆAPI ๅฏ†้’ฅ๏ผ‰ๅ’Œ hook ่„šๆœฌ", - "perm3": "็ณป็ปŸๅทฅๅ…ท๏ผšcurlใ€python3ใ€unameโ€”โ€”ไพ› hook ่„šๆœฌไฝฟ็”จ" + "permissionsTitle": "ๅฎ‰่ฃ…ๅ†…ๅฎน", + "perm1": "claw-market CLIโ€”โ€”่ฝป้‡ๅ‘ฝไปค่กŒๅทฅๅ…ท", + "perm2": "~/.openclaw/โ€”โ€”้…็ฝฎๆ–‡ไปถๅ’Œ่พ…ๅŠฉ่„šๆœฌ", + "perm3": "ๅฎšๆ—ถไปปๅŠกโ€”โ€”ๅฟƒ่ทณๆฃ€ๆต‹๏ผˆ5 ๅˆ†้’Ÿ๏ผ‰ๅ’Œ token ไธŠๆŠฅ๏ผˆ2 ๅฐๆ—ถ๏ผ‰" }, "languageSwitcher": { "en": "EN", diff --git a/package-lock.json b/package-lock.json index d91df4f..fbf79a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "eslint-config-next": "^15.3.0", "postcss": "^8.5.3", "tailwindcss": "^4.1.0", + "tsx": "^4.21.0", "typescript": "^5.8.0" } }, @@ -8316,6 +8317,17 @@ } } }, + "node_modules/next-intl/node_modules/@swc/helpers": { + "version": "0.5.19", + "resolved": "https://mirrors.tencent.com/npm/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next/node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -10335,6 +10347,459 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://mirrors.tencent.com/npm/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://mirrors.tencent.com/npm/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/public/install.sh b/public/install.sh new file mode 100644 index 0000000..ab30fad --- /dev/null +++ b/public/install.sh @@ -0,0 +1,291 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================ +# OpenClaw Market โ€” One-click installer +# Usage: bash <(curl -fsSL https://kymr.top/install.sh) --token +# ============================================================================ + +VERSION="1.0.0" +API_ENDPOINT="https://kymr.top/api/v1" +CONFIG_DIR="$HOME/.openclaw" +CONFIG_FILE="$CONFIG_DIR/config.json" +HEARTBEAT_SCRIPT="$CONFIG_DIR/heartbeat-check.sh" +TRACK_TOKENS_SCRIPT="$CONFIG_DIR/track-tokens.sh" +REPORT_TOKENS_SCRIPT="$CONFIG_DIR/report-tokens.sh" +TOKEN_USAGE_FILE="$CONFIG_DIR/token-usage.json" +CRON_TAG="# openclaw-market" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +info() { echo -e "${GREEN}[โœ“]${NC} $1"; } +warn() { echo -e "${YELLOW}[!]${NC} $1"; } +error() { echo -e "${RED}[โœ—]${NC} $1"; } +step() { echo -e "${CYAN}[โ†’]${NC} $1"; } + +# โ”€โ”€ Parse arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +TOKEN="" +UNINSTALL=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --token) TOKEN="$2"; shift 2 ;; + --token=*) TOKEN="${1#*=}"; shift ;; + --uninstall) UNINSTALL=true; shift ;; + --help|-h) + echo "Usage: bash install.sh --token " + echo "" + echo "Options:" + echo " --token Your OpenClaw token (get it from https://kymr.top)" + echo " --uninstall Remove OpenClaw and all cron jobs" + echo " --help Show this help" + exit 0 + ;; + *) error "Unknown option: $1"; exit 1 ;; + esac +done + +# โ”€โ”€ Uninstall โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +if [ "$UNINSTALL" = true ]; then + step "Uninstalling OpenClaw..." + + # Remove cron jobs + crontab -l 2>/dev/null | grep -v "$CRON_TAG" | crontab - 2>/dev/null || true + info "Cron jobs removed" + + # Remove config directory + if [ -d "$CONFIG_DIR" ]; then + rm -rf "$CONFIG_DIR" + info "Config directory removed: $CONFIG_DIR" + fi + + # Optionally remove CLI + if command -v claw-market &>/dev/null; then + warn "CLI 'claw-market' is still installed. Run 'npm uninstall -g @ricardweii/claw-market' to remove it." + fi + + info "OpenClaw uninstalled successfully!" + exit 0 +fi + +# โ”€โ”€ Validate token โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +if [ -z "$TOKEN" ]; then + error "Token is required. Usage: bash install.sh --token " + echo "" + echo "Get your token at https://kymr.top" + exit 1 +fi + +echo "" +echo -e "${CYAN}๐Ÿฆž OpenClaw Market Installer v${VERSION}${NC}" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" + +# โ”€โ”€ Check prerequisites โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +step "Checking prerequisites..." + +if ! command -v node &>/dev/null; then + error "Node.js is required but not installed." + echo " Install it from https://nodejs.org/ or use nvm:" + echo " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash" + exit 1 +fi +info "Node.js $(node --version) found" + +if ! command -v npm &>/dev/null; then + error "npm is required but not installed." + exit 1 +fi +info "npm $(npm --version) found" + +# โ”€โ”€ Install CLI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +step "Installing claw-market CLI..." + +if command -v claw-market &>/dev/null; then + info "claw-market CLI already installed" +else + npm install -g @ricardweii/claw-market 2>/dev/null || { + warn "Global install failed, trying with sudo..." + sudo npm install -g @ricardweii/claw-market + } + info "claw-market CLI installed" +fi + +# โ”€โ”€ Fetch claw info from server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +step "Verifying token with server..." + +CLAW_INFO=$(curl -sf -H "Authorization: Bearer $TOKEN" "${API_ENDPOINT}/heartbeat" \ + -X POST -H "Content-Type: application/json" -d '{}' 2>/dev/null) || { + error "Invalid token or server unreachable." + echo " Check your token and try again." + exit 1 +} +info "Token verified successfully" + +# โ”€โ”€ Write config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +step "Writing configuration..." + +mkdir -p "$CONFIG_DIR" +chmod 700 "$CONFIG_DIR" + +cat > "$CONFIG_FILE" << EOF +{ + "apiKey": "$TOKEN", + "endpoint": "$API_ENDPOINT" +} +EOF +chmod 600 "$CONFIG_FILE" +info "Config written to $CONFIG_FILE" + +# โ”€โ”€ Create heartbeat check script โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +step "Setting up heartbeat checker..." + +cat > "$HEARTBEAT_SCRIPT" << 'HEARTBEAT_EOF' +#!/usr/bin/env bash +# OpenClaw heartbeat checker โ€” runs every 5 minutes via cron +# Checks if openclaw process is running, sends heartbeat if so + +export PATH="$HOME/.nvm/versions/node/$(ls "$HOME/.nvm/versions/node/" 2>/dev/null | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:$PATH" + +if pgrep -f "openclaw" > /dev/null 2>&1; then + claw-market heartbeat 2>/dev/null || true +fi +HEARTBEAT_EOF +chmod +x "$HEARTBEAT_SCRIPT" +info "Heartbeat checker created" + +# โ”€โ”€ Create token tracking script โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +step "Setting up token tracker..." + +cat > "$TRACK_TOKENS_SCRIPT" << 'TRACK_EOF' +#!/usr/bin/env bash +# OpenClaw token tracker โ€” call with: track-tokens.sh +# Accumulates daily token usage in ~/.openclaw/token-usage.json + +CONFIG_DIR="$HOME/.openclaw" +TOKEN_FILE="$CONFIG_DIR/token-usage.json" +TODAY=$(date +%Y-%m-%d) + +INPUT=${1:-0} +OUTPUT=${2:-0} + +# Validate numeric +if ! [[ "$INPUT" =~ ^[0-9]+$ ]] || ! [[ "$OUTPUT" =~ ^[0-9]+$ ]]; then + echo "Usage: track-tokens.sh " + exit 1 +fi + +# Read existing data or init +if [ -f "$TOKEN_FILE" ]; then + EXISTING_DATE=$(python3 -c "import json; d=json.load(open('$TOKEN_FILE')); print(d.get('date',''))" 2>/dev/null || echo "") + if [ "$EXISTING_DATE" = "$TODAY" ]; then + EXISTING_INPUT=$(python3 -c "import json; d=json.load(open('$TOKEN_FILE')); print(d.get('inputTokens',0))" 2>/dev/null || echo "0") + EXISTING_OUTPUT=$(python3 -c "import json; d=json.load(open('$TOKEN_FILE')); print(d.get('outputTokens',0))" 2>/dev/null || echo "0") + INPUT=$((INPUT + EXISTING_INPUT)) + OUTPUT=$((OUTPUT + EXISTING_OUTPUT)) + fi +fi + +# Write updated data +cat > "$TOKEN_FILE" << EOF +{ + "date": "$TODAY", + "inputTokens": $INPUT, + "outputTokens": $OUTPUT, + "updatedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" +} +EOF + +echo "Token usage updated: input=$INPUT, output=$OUTPUT (date=$TODAY)" +TRACK_EOF +chmod +x "$TRACK_TOKENS_SCRIPT" +info "Token tracker created" + +# โ”€โ”€ Create token reporter script โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +step "Setting up token reporter..." + +cat > "$REPORT_TOKENS_SCRIPT" << 'REPORT_EOF' +#!/usr/bin/env bash +# OpenClaw token reporter โ€” runs every 2 hours via cron +# Reads accumulated token usage and reports to server + +export PATH="$HOME/.nvm/versions/node/$(ls "$HOME/.nvm/versions/node/" 2>/dev/null | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:$PATH" + +CONFIG_DIR="$HOME/.openclaw" +TOKEN_FILE="$CONFIG_DIR/token-usage.json" +TODAY=$(date +%Y-%m-%d) + +# Check if token usage file exists +if [ ! -f "$TOKEN_FILE" ]; then + exit 0 +fi + +# Read data +DATE=$(python3 -c "import json; d=json.load(open('$TOKEN_FILE')); print(d.get('date',''))" 2>/dev/null || echo "") +INPUT=$(python3 -c "import json; d=json.load(open('$TOKEN_FILE')); print(d.get('inputTokens',0))" 2>/dev/null || echo "0") +OUTPUT=$(python3 -c "import json; d=json.load(open('$TOKEN_FILE')); print(d.get('outputTokens',0))" 2>/dev/null || echo "0") + +# Only report if there's data and it's for today +if [ "$INPUT" = "0" ] && [ "$OUTPUT" = "0" ]; then + exit 0 +fi + +# Report via CLI +if [ -n "$DATE" ]; then + claw-market token "$INPUT" "$OUTPUT" --date "$DATE" 2>/dev/null || true +else + claw-market token "$INPUT" "$OUTPUT" 2>/dev/null || true +fi +REPORT_EOF +chmod +x "$REPORT_TOKENS_SCRIPT" +info "Token reporter created" + +# โ”€โ”€ Set up cron jobs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +step "Setting up scheduled tasks..." + +# Remove existing openclaw cron jobs +EXISTING_CRON=$(crontab -l 2>/dev/null | grep -v "$CRON_TAG" || true) + +# Add new cron jobs +NEW_CRON="$EXISTING_CRON +*/5 * * * * $HEARTBEAT_SCRIPT $CRON_TAG +7 */2 * * * $REPORT_TOKENS_SCRIPT $CRON_TAG" + +echo "$NEW_CRON" | crontab - +info "Cron jobs installed:" +echo " - Heartbeat check: every 5 minutes" +echo " - Token report: every 2 hours" + +# โ”€โ”€ Send initial heartbeat โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +step "Sending initial heartbeat..." + +claw-market heartbeat 2>/dev/null && { + info "Initial heartbeat sent" +} || { + warn "Initial heartbeat failed (non-blocking)" +} + +# โ”€โ”€ Done โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +echo "" +echo -e "${GREEN}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}" +echo -e "${GREEN}๐Ÿฆž OpenClaw installed successfully!${NC}" +echo "" +echo " Your claw is now active on the heatmap." +echo " View it at: https://kymr.top" +echo "" +echo " Scripts installed:" +echo " $HEARTBEAT_SCRIPT โ€” heartbeat (every 5 min)" +echo " $TRACK_TOKENS_SCRIPT โ€” track token usage" +echo " $REPORT_TOKENS_SCRIPT โ€” report tokens (every 2 hr)" +echo "" +echo " To track tokens manually:" +echo " ~/.openclaw/track-tokens.sh " +echo "" +echo " To uninstall:" +echo " bash <(curl -fsSL https://kymr.top/install.sh) --uninstall" +echo ""