");
+ });
});
describe("MarkdownPreview — standard markdown still renders", () => {
- it("renders ATX headings", () => {
- const container = renderMarkdown(`## Why This Plugin`);
- const h2 = container.querySelector("h2");
- expect(h2?.textContent).toBe("Why This Plugin");
- });
+ it("renders ATX headings", () => {
+ const container = renderMarkdown(`## Why This Plugin`);
+ const h2 = container.querySelector("h2");
+ expect(h2?.textContent).toBe("Why This Plugin");
+ });
- it("renders markdown links", () => {
- const container = renderMarkdown(`[Opik](https://example.com/opik)`);
- const a = container.querySelector("a");
- expect(a?.getAttribute("href")).toBe("https://example.com/opik");
- expect(a?.textContent).toBe("Opik");
- });
+ it("renders markdown links", () => {
+ const container = renderMarkdown(`[Opik](https://example.com/opik)`);
+ const a = container.querySelector("a");
+ expect(a?.getAttribute("href")).toBe("https://example.com/opik");
+ expect(a?.textContent).toBe("Opik");
+ });
- it("renders inline code", () => {
- const container = renderMarkdown("Use `@opik/opik-openclaw` now.");
- const code = container.querySelector("code");
- expect(code?.textContent).toBe("@opik/opik-openclaw");
- });
+ it("renders inline code", () => {
+ const container = renderMarkdown("Use `@opik/opik-openclaw` now.");
+ const code = container.querySelector("code");
+ expect(code?.textContent).toBe("@opik/opik-openclaw");
+ });
- it("renders unordered lists", () => {
- const container = renderMarkdown(`- one\n- two\n- three`);
- const items = container.querySelectorAll("li");
- expect(items.length).toBe(3);
- expect(items[0].textContent).toBe("one");
- });
+ it("renders unordered lists", () => {
+ const container = renderMarkdown(`- one\n- two\n- three`);
+ const items = container.querySelectorAll("li");
+ expect(items.length).toBe(3);
+ expect(items[0].textContent).toBe("one");
+ });
- it("renders GFM tables", () => {
- const container = renderMarkdown(
- ["| Key | Value |", "| --- | ----- |", "| a | 1 |", "| b | 2 |"].join("\n"),
- );
- expect(container.querySelector("table")).not.toBeNull();
- expect(container.querySelectorAll("tbody tr").length).toBe(2);
- });
+ it("renders GFM tables", () => {
+ const container = renderMarkdown(
+ ["| Key | Value |", "| --- | ----- |", "| a | 1 |", "| b | 2 |"].join("\n"),
+ );
+ expect(container.querySelector("table")).not.toBeNull();
+ expect(container.querySelectorAll("tbody tr").length).toBe(2);
+ });
- it("renders fenced code blocks as ", () => {
- const container = renderMarkdown("```ts\nconst x = 1;\n```");
- const code = container.querySelector("pre code");
- expect(code).not.toBeNull();
- expect(code?.textContent).toContain("const x = 1;");
- });
+ it("renders fenced code blocks as ", () => {
+ const container = renderMarkdown("```ts\nconst x = 1;\n```");
+ const code = container.querySelector("pre code");
+ expect(code).not.toBeNull();
+ expect(code?.textContent).toContain("const x = 1;");
+ });
});
describe("MarkdownPreview — syntax highlighting", () => {
- it("shiki-highlights fenced code blocks (produces colored tokens)", async () => {
- const { container } = render(
- {"```ts\nconst x: number = 1;\n```"} ,
- );
+ it("shiki-highlights fenced code blocks (produces colored tokens)", async () => {
+ const { container } = render(
+ {"```ts\nconst x: number = 1;\n```"} ,
+ );
- await waitFor(
- () => {
- const pre = container.querySelector("pre");
- // Shiki wraps the output in and tokens are
- // .
- expect(pre?.className ?? "").toMatch(/shiki/);
- const coloredSpans = container.querySelectorAll("pre span[style*='color']");
- expect(coloredSpans.length).toBeGreaterThan(0);
- },
- { timeout: 8000 },
- );
+ await waitFor(
+ () => {
+ const pre = container.querySelector("pre");
+ // Shiki wraps the output in and tokens are
+ // .
+ expect(pre?.className ?? "").toMatch(/shiki/);
+ const coloredSpans = container.querySelectorAll("pre span[style*='color']");
+ expect(coloredSpans.length).toBeGreaterThan(0);
+ },
+ { timeout: 8000 },
+ );
- // Raw code text must still be present after highlighting
- expect(container.querySelector("pre")?.textContent).toContain("const x");
- });
+ // Raw code text must still be present after highlighting
+ expect(container.querySelector("pre")?.textContent).toContain("const x");
+ });
- it("leaves the highlight prop honored — highlight={false} renders plain ", () => {
- const { container } = render(
- {"```ts\nconst x = 1;\n```"} ,
- );
- const pre = container.querySelector("pre");
- // No shiki class, no colored spans
- expect(pre?.className ?? "").not.toMatch(/shiki/);
- expect(container.querySelectorAll("pre span[style*='color']").length).toBe(0);
- expect(pre?.textContent).toContain("const x = 1;");
- });
+ it("leaves the highlight prop honored — highlight={false} renders plain ", () => {
+ const { container } = render(
+ {"```ts\nconst x = 1;\n```"} ,
+ );
+ const pre = container.querySelector("pre");
+ // No shiki class, no colored spans
+ expect(pre?.className ?? "").not.toMatch(/shiki/);
+ expect(container.querySelectorAll("pre span[style*='color']").length).toBe(0);
+ expect(pre?.textContent).toContain("const x = 1;");
+ });
});
describe("MarkdownPreview — sanitization of malicious HTML", () => {
- it("strips world`);
- expect(container.querySelector("script")).toBeNull();
- expect(container.textContent ?? "").not.toContain("window.__pwn");
- });
+ it("strips world`);
+ expect(container.querySelector("script")).toBeNull();
+ expect(container.textContent ?? "").not.toContain("window.__pwn");
+ });
- it("strips onerror handlers on ", () => {
- const container = renderMarkdown(` `);
- const img = container.querySelector("img");
- // The img itself can render; the handler must be gone.
- expect(img?.getAttribute("onerror")).toBeNull();
- });
+ it("strips onerror handlers on ", () => {
+ const container = renderMarkdown(` `);
+ const img = container.querySelector("img");
+ // The img itself can render; the handler must be gone.
+ expect(img?.getAttribute("onerror")).toBeNull();
+ });
- it.each([
- "javascript:alert(1)",
- "JaVaScRiPt:alert(1)",
- "data:text/html,",
- "vbscript:msgbox(1)",
- ])("strips unsafe hrefs on anchors: %s", (unsafeHref) => {
- const container = renderMarkdown(`click `);
- const a = container.querySelector("a");
- // Either the href is removed entirely or rewritten; it must not keep an executable scheme.
- const href = a?.getAttribute("href") ?? "";
- expect(href).not.toMatch(/^\s*(javascript|data|vbscript):/i);
- });
+ it.each([
+ "javascript:alert(1)",
+ "JaVaScRiPt:alert(1)",
+ "data:text/html,",
+ "vbscript:msgbox(1)",
+ ])("strips unsafe hrefs on anchors: %s", (unsafeHref) => {
+ const container = renderMarkdown(`click `);
+ const a = container.querySelector("a");
+ // Either the href is removed entirely or rewritten; it must not keep an executable scheme.
+ const href = a?.getAttribute("href") ?? "";
+ expect(href).not.toMatch(/^\s*(javascript|data|vbscript):/i);
+ });
});
diff --git a/src/components/MarketplaceIcon.tsx b/src/components/MarketplaceIcon.tsx
index 3e3d6a24..e4bed699 100644
--- a/src/components/MarketplaceIcon.tsx
+++ b/src/components/MarketplaceIcon.tsx
@@ -33,12 +33,7 @@ function getIcon(kind: MarketplaceIconProps["kind"]) {
}
}
-export function MarketplaceIcon({
- kind,
- label,
- imageUrl,
- size = "sm",
-}: MarketplaceIconProps) {
+export function MarketplaceIcon({ kind, label, imageUrl, size = "sm" }: MarketplaceIconProps) {
const Icon = getIcon(kind);
const tone = hashTone(label);
diff --git a/src/components/PackageSourceChooser.tsx b/src/components/PackageSourceChooser.tsx
index f4f8875d..91deac00 100644
--- a/src/components/PackageSourceChooser.tsx
+++ b/src/components/PackageSourceChooser.tsx
@@ -9,7 +9,7 @@ import { Button } from "./ui/button";
import { Card } from "./ui/card";
const OPENCLAW_PLUGIN_PACKAGE_METADATA_DOCS_URL =
- 'https://docs.openclaw.ai/plugins/sdk-setup#package-metadata';
+ "https://docs.openclaw.ai/plugins/sdk-setup#package-metadata";
export function PackageSourceChooser(props: {
files: File[];
diff --git a/src/components/PluginListItem.tsx b/src/components/PluginListItem.tsx
index adad956c..c6f8caa1 100644
--- a/src/components/PluginListItem.tsx
+++ b/src/components/PluginListItem.tsx
@@ -1,8 +1,8 @@
import { Link } from "@tanstack/react-router";
+import type { PackageListItem } from "../lib/packageApi";
+import { familyLabel } from "../lib/packageLabels";
import { MarketplaceIcon } from "./MarketplaceIcon";
import { Badge } from "./ui/badge";
-import { familyLabel } from "../lib/packageLabels";
-import type { PackageListItem } from "../lib/packageApi";
type PluginListItemProps = {
item: PackageListItem;
@@ -10,7 +10,12 @@ type PluginListItemProps = {
export function PluginListItem({ item }: PluginListItemProps) {
return (
-
+
@@ -24,7 +29,9 @@ export function PluginListItem({ item }: PluginListItemProps) {
{familyLabel(item.family)}
{item.isOfficial ? Verified : null}
-
{item.summary ?? "Plugin package for agent workflows."}
+
+ {item.summary ?? "Plugin package for agent workflows."}
+
Plugin
{item.latestVersion ? (
diff --git a/src/components/SignInButton.tsx b/src/components/SignInButton.tsx
index 4d067dd6..0fcb10b2 100644
--- a/src/components/SignInButton.tsx
+++ b/src/components/SignInButton.tsx
@@ -10,11 +10,7 @@ type SignInButtonProps = Omit
& {
redirectTo?: string;
};
-export function SignInButton({
- redirectTo,
- children = "Sign In",
- ...props
-}: SignInButtonProps) {
+export function SignInButton({ redirectTo, children = "Sign In", ...props }: SignInButtonProps) {
const { signIn } = useAuthActions();
return (
@@ -32,9 +28,7 @@ export function SignInButton({
}
})
.catch((error) => {
- setAuthError(
- getUserFacingAuthError(error, "Sign in failed. Please try again."),
- );
+ setAuthError(getUserFacingAuthError(error, "Sign in failed. Please try again."));
});
}}
>
diff --git a/src/components/SkillCard.tsx b/src/components/SkillCard.tsx
index 60aa5fe4..7076ff69 100644
--- a/src/components/SkillCard.tsx
+++ b/src/components/SkillCard.tsx
@@ -1,8 +1,8 @@
import { Link } from "@tanstack/react-router";
import type { ReactNode } from "react";
+import type { PublicSkill } from "../lib/publicUser";
import { MarketplaceIcon } from "./MarketplaceIcon";
import { Badge } from "./ui/badge";
-import type { PublicSkill } from "../lib/publicUser";
type SkillCardProps = {
skill: PublicSkill;
@@ -33,9 +33,7 @@ export function SkillCard({
{hasTags ? (
{badges.map((label) => (
-
- {label}
-
+
{label}
))}
{chip ?
{chip} : null}
{platformLabels?.map((label) => (
diff --git a/src/components/SkillDiffCard.test.tsx b/src/components/SkillDiffCard.test.tsx
index 5c71dac3..8aa1e9e9 100644
--- a/src/components/SkillDiffCard.test.tsx
+++ b/src/components/SkillDiffCard.test.tsx
@@ -20,12 +20,7 @@ vi.mock("@monaco-editor/react", () => ({
}: {
className?: string;
options?: { renderSideBySide?: boolean; useInlineViewWhenSpaceIsLimited?: boolean };
- }) => (
-
- ),
+ }) =>
,
useMonaco: () => null,
}));
diff --git a/src/components/SkillDiffCard.tsx b/src/components/SkillDiffCard.tsx
index 2ee6bcc0..98cd5640 100644
--- a/src/components/SkillDiffCard.tsx
+++ b/src/components/SkillDiffCard.tsx
@@ -14,8 +14,8 @@ import {
sortVersionsBySemver,
} from "../lib/diffing";
import { isDarkThemeResolved, onThemeChange } from "../lib/theme";
-import { Button } from "./ui/button";
import { ClientOnly } from "./ClientOnly";
+import { Button } from "./ui/button";
type SkillDiffCardProps = {
skill: Doc<"skills">;
@@ -283,12 +283,8 @@ export function SkillDiffCard({ skill, versions, variant = "card" }: SkillDiffCa
-
- Compare versions
-
-
- Inline or side-by-side diff for any file.
-
+
Compare versions
+
Inline or side-by-side diff for any file.
{!diffUnavailable ? (
diff --git a/src/components/SkillFilesPanel.tsx b/src/components/SkillFilesPanel.tsx
index 0ee8d100..7f006b5e 100644
--- a/src/components/SkillFilesPanel.tsx
+++ b/src/components/SkillFilesPanel.tsx
@@ -11,10 +11,7 @@ type SkillFilesPanelProps = {
latestFiles: SkillFile[];
};
-export function SkillFilesPanel({
- versionId,
- latestFiles,
-}: SkillFilesPanelProps) {
+export function SkillFilesPanel({ versionId, latestFiles }: SkillFilesPanelProps) {
const getFileText = useAction(api.skills.getFileText);
const [selectedPath, setSelectedPath] = useState(null);
const [fileContent, setFileContent] = useState(null);
@@ -89,12 +86,8 @@ export function SkillFilesPanel({
-
- Files
-
-
- {latestFiles.length} total
-
+ Files
+ {latestFiles.length} total
{latestFiles.length === 0 ? (
diff --git a/src/components/SkillInstallSurface.test.tsx b/src/components/SkillInstallSurface.test.tsx
index ce1ee94b..c083d877 100644
--- a/src/components/SkillInstallSurface.test.tsx
+++ b/src/components/SkillInstallSurface.test.tsx
@@ -11,13 +11,7 @@ vi.mock("./ui/dropdown-menu", () => ({
DropdownMenu: ({ children }: { children: ReactNode }) =>
{children}
,
DropdownMenuTrigger: ({ children }: { children: ReactNode }) =>
{children}
,
DropdownMenuContent: ({ children }: { children: ReactNode }) =>
{children}
,
- DropdownMenuItem: ({
- children,
- onSelect,
- }: {
- children: ReactNode;
- onSelect?: () => void;
- }) => (
+ DropdownMenuItem: ({ children, onSelect }: { children: ReactNode; onSelect?: () => void }) => (
onSelect?.()}>
{children}
@@ -85,9 +79,7 @@ describe("SkillInstallSurface", () => {
expect(screen.getByText("openclaw skills install weather")).toBeTruthy();
expect(screen.queryByText("npx clawhub@latest install weather")).toBeNull();
expect(screen.getByRole("tab", { name: "CLI" }).getAttribute("aria-selected")).toBe("true");
- expect(screen.getByRole("tab", { name: "Prompt" }).getAttribute("aria-selected")).toBe(
- "false",
- );
+ expect(screen.getByRole("tab", { name: "Prompt" }).getAttribute("aria-selected")).toBe("false");
fireEvent.click(screen.getByRole("button", { name: "Copy OpenClaw CLI command" }));
@@ -98,9 +90,7 @@ describe("SkillInstallSurface", () => {
fireEvent.click(screen.getByRole("tab", { name: "Prompt" }));
expect(screen.getByText(/Install the skill "Weather"/i)).toBeTruthy();
- expect(screen.getByRole("tab", { name: "Prompt" }).getAttribute("aria-selected")).toBe(
- "true",
- );
+ expect(screen.getByRole("tab", { name: "Prompt" }).getAttribute("aria-selected")).toBe("true");
fireEvent.click(screen.getByRole("button", { name: "Copy OpenClaw prompt" }));
diff --git a/src/components/SkillInstallSurface.tsx b/src/components/SkillInstallSurface.tsx
index c41925e6..13bd855c 100644
--- a/src/components/SkillInstallSurface.tsx
+++ b/src/components/SkillInstallSurface.tsx
@@ -129,8 +129,8 @@ export function SkillInstallSurface({
OpenClaw Prompt Flow
Install with OpenClaw
- Best for remote or guided setup. Copy the exact prompt, then paste it into OpenClaw
- for {installTarget}.
+ Best for remote or guided setup. Copy the exact prompt, then paste it into OpenClaw for{" "}
+ {installTarget}.
@@ -207,9 +207,7 @@ export function SkillCommandLineCard({
type="button"
role="tab"
aria-selected={activeInstallTab === "prompt"}
- className={`install-switcher-pill${
- activeInstallTab === "prompt" ? " is-active" : ""
- }`}
+ className={`install-switcher-pill${activeInstallTab === "prompt" ? " is-active" : ""}`}
onClick={() => setActiveInstallTab("prompt")}
>
Prompt
@@ -230,9 +228,7 @@ export function SkillCommandLineCard({
@@ -128,24 +127,31 @@ function UserStatsTooltipContent({
{displayName}
)}
- {handle && (
- @{handle}
- )}
+ {handle && @{handle} }
{stats === null ? (
Loading...
) : (
<>
-
+
{formatCompactStat(stats.publishedSkills)}
-
+
{formatCompactStat(stats.totalStars)}
-
+
{formatCompactStat(stats.totalDownloads)}
diff --git a/src/components/UserListItem.tsx b/src/components/UserListItem.tsx
index cd3a3f93..f1ea0c58 100644
--- a/src/components/UserListItem.tsx
+++ b/src/components/UserListItem.tsx
@@ -1,6 +1,6 @@
import { Link } from "@tanstack/react-router";
-import { MarketplaceIcon } from "./MarketplaceIcon";
import type { PublicUser } from "../lib/publicUser";
+import { MarketplaceIcon } from "./MarketplaceIcon";
type UserListItemProps = {
user: PublicUser;
@@ -13,7 +13,12 @@ export function UserListItem({ user }: UserListItemProps) {
const displayName = user.displayName ?? user.name ?? handle;
return (
-
+
diff --git a/src/components/layout/Container.tsx b/src/components/layout/Container.tsx
index f3c8e68e..b28f3767 100644
--- a/src/components/layout/Container.tsx
+++ b/src/components/layout/Container.tsx
@@ -2,23 +2,23 @@ import * as React from "react";
import { cn } from "../../lib/utils";
interface ContainerProps extends React.HTMLAttributes
{
- size?: "default" | "narrow" | "wide";
+ size?: "default" | "narrow" | "wide";
}
const Container = React.forwardRef(
- ({ className, size = "default", ...props }, ref) => (
-
- ),
+ ({ className, size = "default", ...props }, ref) => (
+
+ ),
);
Container.displayName = "Container";
diff --git a/src/components/skeletons/SkillDetailSkeleton.tsx b/src/components/skeletons/SkillDetailSkeleton.tsx
index 07243573..272d9ed5 100644
--- a/src/components/skeletons/SkillDetailSkeleton.tsx
+++ b/src/components/skeletons/SkillDetailSkeleton.tsx
@@ -59,7 +59,6 @@ export function SkillDetailSkeleton() {
-
);
diff --git a/src/components/skillDetailUtils.test.ts b/src/components/skillDetailUtils.test.ts
index c3a25167..a41a457d 100644
--- a/src/components/skillDetailUtils.test.ts
+++ b/src/components/skillDetailUtils.test.ts
@@ -13,7 +13,9 @@ describe("skill detail install helpers", () => {
const ownerPublisherId = "publishers:1" as Id<"publishers">;
it("prefers the owner handle for install targets", () => {
- expect(buildSkillInstallTarget("steipete", ownerPublisherId, "weather")).toBe("steipete/weather");
+ expect(buildSkillInstallTarget("steipete", ownerPublisherId, "weather")).toBe(
+ "steipete/weather",
+ );
});
it("falls back to owner id and then plain slug", () => {
@@ -23,11 +25,15 @@ describe("skill detail install helpers", () => {
it("formats the OpenClaw and ClawHub commands", () => {
expect(formatOpenClawInstallCommand("weather")).toBe("openclaw skills install weather");
- expect(formatClawHubInstallCommand("weather", "npm")).toBe("npx clawhub@latest install weather");
+ expect(formatClawHubInstallCommand("weather", "npm")).toBe(
+ "npx clawhub@latest install weather",
+ );
expect(formatClawHubInstallCommand("weather", "pnpm")).toBe(
"pnpm dlx clawhub@latest install weather",
);
- expect(formatClawHubInstallCommand("weather", "bun")).toBe("bunx clawhub@latest install weather");
+ expect(formatClawHubInstallCommand("weather", "bun")).toBe(
+ "bunx clawhub@latest install weather",
+ );
});
it("builds the install-and-setup prompt from known metadata only", () => {
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
index 583a3a5a..d1893f20 100644
--- a/src/components/ui/badge.tsx
+++ b/src/components/ui/badge.tsx
@@ -15,11 +15,16 @@ const Badge = React.forwardRef(
// Variant styles — all token-driven, no dark: overrides needed
variant === "default" && "bg-hover-bg px-3 py-1 text-ink-soft border border-line",
variant === "accent" && "bg-active-bg px-3 py-1 text-accent-deep border border-line",
- variant === "compact" && "bg-hover-bg px-2.5 py-0.5 text-fs-xs text-ink-soft border border-line",
- variant === "pending" && "bg-status-warning-bg px-3 py-1 text-status-warning-fg border border-line",
- variant === "success" && "bg-status-success-bg px-3 py-1 text-status-success-fg border border-line",
- variant === "warning" && "bg-status-warning-bg px-3 py-1 text-status-warning-fg border border-line",
- variant === "destructive" && "bg-status-error-bg px-3 py-1 text-status-error-fg border border-line",
+ variant === "compact" &&
+ "bg-hover-bg px-2.5 py-0.5 text-fs-xs text-ink-soft border border-line",
+ variant === "pending" &&
+ "bg-status-warning-bg px-3 py-1 text-status-warning-fg border border-line",
+ variant === "success" &&
+ "bg-status-success-bg px-3 py-1 text-status-success-fg border border-line",
+ variant === "warning" &&
+ "bg-status-warning-bg px-3 py-1 text-status-warning-fg border border-line",
+ variant === "destructive" &&
+ "bg-status-error-bg px-3 py-1 text-status-error-fg border border-line",
className,
)}
{...props}
diff --git a/src/lib/categories.ts b/src/lib/categories.ts
index fb538ceb..5305b774 100644
--- a/src/lib/categories.ts
+++ b/src/lib/categories.ts
@@ -7,12 +7,42 @@ export type SkillCategory = {
export const SKILL_CATEGORIES: SkillCategory[] = [
{ slug: "mcp-tools", label: "MCP Tools", icon: "plug", keywords: ["mcp", "tool", "server"] },
- { slug: "prompts", label: "Prompts", icon: "message-square", keywords: ["prompt", "template", "system"] },
- { slug: "workflows", label: "Workflows", icon: "git-branch", keywords: ["workflow", "pipeline", "chain"] },
- { slug: "dev-tools", label: "Dev Tools", icon: "wrench", keywords: ["dev", "debug", "lint", "test", "build"] },
- { slug: "data", label: "Data & APIs", icon: "database", keywords: ["api", "data", "fetch", "http", "rest", "graphql"] },
- { slug: "security", label: "Security", icon: "shield", keywords: ["security", "scan", "auth", "encrypt"] },
- { slug: "automation", label: "Automation", icon: "zap", keywords: ["auto", "cron", "schedule", "bot"] },
+ {
+ slug: "prompts",
+ label: "Prompts",
+ icon: "message-square",
+ keywords: ["prompt", "template", "system"],
+ },
+ {
+ slug: "workflows",
+ label: "Workflows",
+ icon: "git-branch",
+ keywords: ["workflow", "pipeline", "chain"],
+ },
+ {
+ slug: "dev-tools",
+ label: "Dev Tools",
+ icon: "wrench",
+ keywords: ["dev", "debug", "lint", "test", "build"],
+ },
+ {
+ slug: "data",
+ label: "Data & APIs",
+ icon: "database",
+ keywords: ["api", "data", "fetch", "http", "rest", "graphql"],
+ },
+ {
+ slug: "security",
+ label: "Security",
+ icon: "shield",
+ keywords: ["security", "scan", "auth", "encrypt"],
+ },
+ {
+ slug: "automation",
+ label: "Automation",
+ icon: "zap",
+ keywords: ["auto", "cron", "schedule", "bot"],
+ },
{ slug: "other", label: "Other", icon: "package", keywords: [] },
];
diff --git a/src/lib/convexError.ts b/src/lib/convexError.ts
index 259449f1..82e8a113 100644
--- a/src/lib/convexError.ts
+++ b/src/lib/convexError.ts
@@ -28,10 +28,7 @@ export function getUserFacingConvexError(error: unknown, fallback: string) {
if (hasOwnProperty(maybe, "data")) {
if (typeof maybe.data === "string") candidates.push(maybe.data);
- if (
- hasOwnProperty(maybe.data, "message") &&
- typeof maybe.data.message === "string"
- ) {
+ if (hasOwnProperty(maybe.data, "message") && typeof maybe.data.message === "string") {
candidates.push(maybe.data.message);
}
}
diff --git a/src/lib/hasOwnProperty.ts b/src/lib/hasOwnProperty.ts
index 65262f9d..ca842daf 100644
--- a/src/lib/hasOwnProperty.ts
+++ b/src/lib/hasOwnProperty.ts
@@ -2,5 +2,7 @@ export function hasOwnProperty(
value: unknown,
key: K,
): value is Record {
- return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, key);
+ return (
+ typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, key)
+ );
}
diff --git a/src/lib/useAuthError.ts b/src/lib/useAuthError.ts
index 9a7fc850..2ecc03c7 100644
--- a/src/lib/useAuthError.ts
+++ b/src/lib/useAuthError.ts
@@ -6,33 +6,29 @@ let authError: string | null = null;
const listeners = new Set<() => void>();
function emitChange() {
- for (const listener of listeners) listener();
+ for (const listener of listeners) listener();
}
function subscribe(listener: () => void) {
- listeners.add(listener);
- return () => listeners.delete(listener);
+ listeners.add(listener);
+ return () => listeners.delete(listener);
}
export function getAuthErrorSnapshot() {
- return authError;
+ return authError;
}
export function setAuthError(error: string | null) {
- if (authError === error) return;
- authError = error;
- emitChange();
+ if (authError === error) return;
+ authError = error;
+ emitChange();
}
export function clearAuthError() {
- setAuthError(null);
+ setAuthError(null);
}
export function useAuthError() {
- const error = useSyncExternalStore(
- subscribe,
- getAuthErrorSnapshot,
- getAuthErrorSnapshot,
- );
- return { error, clear: clearAuthError };
+ const error = useSyncExternalStore(subscribe, getAuthErrorSnapshot, getAuthErrorSnapshot);
+ return { error, clear: clearAuthError };
}
diff --git a/src/routes/$owner/$slug/security/$scanner.tsx b/src/routes/$owner/$slug/security/$scanner.tsx
index 7743e8e5..2ed6ae8b 100644
--- a/src/routes/$owner/$slug/security/$scanner.tsx
+++ b/src/routes/$owner/$slug/security/$scanner.tsx
@@ -1,10 +1,7 @@
import { createFileRoute, notFound, redirect } from "@tanstack/react-router";
import { useQuery } from "convex/react";
import { api } from "../../../../../convex/_generated/api";
-import {
- SecurityScannerPage,
- type ScannerSlug,
-} from "../../../../components/SecurityScannerPage";
+import { SecurityScannerPage, type ScannerSlug } from "../../../../components/SecurityScannerPage";
import { buildSkillMeta } from "../../../../lib/og";
import { fetchSkillPageData } from "../../../../lib/skillPage";
diff --git a/src/routes/dashboard.test.tsx b/src/routes/-dashboard.test.tsx
similarity index 98%
rename from src/routes/dashboard.test.tsx
rename to src/routes/-dashboard.test.tsx
index b1fd84c9..acc7259d 100644
--- a/src/routes/dashboard.test.tsx
+++ b/src/routes/-dashboard.test.tsx
@@ -242,7 +242,9 @@ describe("Dashboard minimal rows", () => {
expect(screen.getByText("Blocked")).toBeTruthy();
expect(screen.getByRole("button", { name: "Suspicious status reason" })).toBeTruthy();
expect(screen.getByRole("button", { name: "Blocked status reason" })).toBeTruthy();
- expect(screen.getByRole("button", { name: "Open actions for Local Flagged Skill" })).toBeTruthy();
+ expect(
+ screen.getByRole("button", { name: "Open actions for Local Flagged Skill" }),
+ ).toBeTruthy();
expect(
screen.getByRole("button", { name: "Open actions for Local Flagged Runtime Plugin" }),
).toBeTruthy();
@@ -289,5 +291,4 @@ describe("Dashboard minimal rows", () => {
expect(screen.queryByText("2/3 rescans left")).toBeNull();
expect(screen.queryByText("Limit reached (3/3)")).toBeNull();
});
-
});
diff --git a/src/routes/about.tsx b/src/routes/about.tsx
index de605955..e44d48a1 100644
--- a/src/routes/about.tsx
+++ b/src/routes/about.tsx
@@ -1,23 +1,15 @@
-import { createFileRoute, Link } from '@tanstack/react-router';
-import type { LucideIcon } from 'lucide-react';
-import {
- Banknote,
- Drama,
- Eye,
- EyeOff,
- ImageOff,
- ShieldOff,
- UserX,
-} from 'lucide-react';
-import type { ReactNode } from 'react';
-import { Badge } from '../components/ui/badge';
-import { Button } from '../components/ui/button';
-import { getSiteMode, getSiteName, getSiteUrlForMode } from '../lib/site';
+import { createFileRoute, Link } from "@tanstack/react-router";
+import type { LucideIcon } from "lucide-react";
+import { Banknote, Drama, Eye, EyeOff, ImageOff, ShieldOff, UserX } from "lucide-react";
+import type { ReactNode } from "react";
+import { Badge } from "../components/ui/badge";
+import { Button } from "../components/ui/button";
+import { getSiteMode, getSiteName, getSiteUrlForMode } from "../lib/site";
export function renderWithInlineCode(text: string): ReactNode[] {
const parts = text.split(/(`[^`]+`)/g);
return parts.map((part, i) => {
- if (part.startsWith('`') && part.endsWith('`')) {
+ if (part.startsWith("`") && part.endsWith("`")) {
return (
{part.slice(1, -1)}
@@ -30,68 +22,68 @@ export function renderWithInlineCode(text: string): ReactNode[] {
const prohibitedCategories: { title: string; icon: LucideIcon; examples: string }[] = [
{
- title: 'Bypass and unauthorized access',
+ title: "Bypass and unauthorized access",
icon: ShieldOff,
examples:
- 'Auth bypass, account takeover, CAPTCHA bypass, Cloudflare or anti-bot evasion, rate-limit bypass, reusable session theft, live call or agent takeover.',
+ "Auth bypass, account takeover, CAPTCHA bypass, Cloudflare or anti-bot evasion, rate-limit bypass, reusable session theft, live call or agent takeover.",
},
{
- title: 'Platform abuse and ban evasion',
+ title: "Platform abuse and ban evasion",
icon: UserX,
examples:
- 'Stealth accounts after bans, account warming/farming, fake engagement, multi-account automation, spam posting, marketplace or social automation built to avoid detection.',
+ "Stealth accounts after bans, account warming/farming, fake engagement, multi-account automation, spam posting, marketplace or social automation built to avoid detection.",
},
{
- title: 'Fraud and deception',
+ title: "Fraud and deception",
icon: Banknote,
examples:
- 'Fake certificates, fake invoices, deceptive payment flows, fake social proof, scam outreach, or synthetic-identity workflows built to create accounts for fraud.',
+ "Fake certificates, fake invoices, deceptive payment flows, fake social proof, scam outreach, or synthetic-identity workflows built to create accounts for fraud.",
},
{
- title: 'Privacy-invasive surveillance',
+ title: "Privacy-invasive surveillance",
icon: Eye,
examples:
- 'Mass contact scraping for spam, doxxing, stalking, covert monitoring, biometric / face-matching workflows without clear consent, or buying, publishing, downloading, or operationalizing leaked data or breach dumps.',
+ "Mass contact scraping for spam, doxxing, stalking, covert monitoring, biometric / face-matching workflows without clear consent, or buying, publishing, downloading, or operationalizing leaked data or breach dumps.",
},
{
- title: 'Non-consensual impersonation',
+ title: "Non-consensual impersonation",
icon: Drama,
examples:
- 'Face swap, digital twins, cloned influencers, fake personas, or other identity manipulation used to impersonate or mislead.',
+ "Face swap, digital twins, cloned influencers, fake personas, or other identity manipulation used to impersonate or mislead.",
},
{
- title: 'Explicit sexual content',
+ title: "Explicit sexual content",
icon: ImageOff,
examples:
- 'NSFW image, video, or text generation, especially wrappers around third-party APIs with safety checks disabled.',
+ "NSFW image, video, or text generation, especially wrappers around third-party APIs with safety checks disabled.",
},
{
- title: 'Hidden or misleading execution',
+ title: "Hidden or misleading execution",
icon: EyeOff,
examples:
- 'Obfuscated install commands, `curl | sh`, undeclared secret requirements, undeclared private-key use, or remote `npx @latest` execution without reviewability.',
+ "Obfuscated install commands, `curl | sh`, undeclared secret requirements, undeclared private-key use, or remote `npx @latest` execution without reviewability.",
},
];
const recentPatterns = [
- 'Create stealth seller accounts after marketplace bans.',
- 'Modify Telegram pairing so unapproved users automatically receive pairing codes.',
- 'Cultivate Reddit or Twitter accounts with undetectable automation.',
- 'Generate professional certificates or invoices for arbitrary use.',
- 'Generate NSFW content with safety checks disabled.',
- 'Scrape leads, enrich contacts, and launch cold outreach at scale.',
- 'Buy, publish, or download leaked data or breach dumps.',
- 'Bulk-create email or social accounts with synthetic identities or CAPTCHA solving.',
+ "Create stealth seller accounts after marketplace bans.",
+ "Modify Telegram pairing so unapproved users automatically receive pairing codes.",
+ "Cultivate Reddit or Twitter accounts with undetectable automation.",
+ "Generate professional certificates or invoices for arbitrary use.",
+ "Generate NSFW content with safety checks disabled.",
+ "Scrape leads, enrich contacts, and launch cold outreach at scale.",
+ "Buy, publish, or download leaked data or breach dumps.",
+ "Bulk-create email or social accounts with synthetic identities or CAPTCHA solving.",
];
-export const Route = createFileRoute('/about')({
+export const Route = createFileRoute("/about")({
head: () => {
const mode = getSiteMode();
const siteName = getSiteName(mode);
const siteUrl = getSiteUrlForMode(mode);
const title = `About · ${siteName}`;
const description =
- 'What ClawHub allows, what we do not host, and the abuse patterns that lead to removal or account bans.';
+ "What ClawHub allows, what we do not host, and the abuse patterns that lead to removal or account bans.";
return {
links: [
@@ -102,11 +94,11 @@ export const Route = createFileRoute('/about')({
],
meta: [
{ title },
- { name: 'description', content: description },
- { property: 'og:title', content: title },
- { property: 'og:description', content: description },
- { property: 'og:type', content: 'website' },
- { property: 'og:url', content: `${siteUrl}/about` },
+ { name: "description", content: description },
+ { property: "og:title", content: title },
+ { property: "og:description", content: description },
+ { property: "og:type", content: "website" },
+ { property: "og:url", content: `${siteUrl}/about` },
],
};
},
@@ -125,9 +117,9 @@ function AboutPage() {
What ClawHub will not host
- ClawHub is for useful agent tooling, not abuse workflows. If a skill is built to
- evade defenses, scam people, invade privacy, or enable non-consensual behavior, it
- does not belong here.
+ ClawHub is for useful agent tooling, not abuse workflows. If a skill is built to evade
+ defenses, scam people, invade privacy, or enable non-consensual behavior, it does not
+ belong here.
@@ -203,9 +195,7 @@ function AboutPage() {
-
- Browse Skills
-
+ Browse Skills
({
}));
vi.mock("../../components/SignInButton", () => ({
- SignInButton: ({
- children,
- ...props
- }: React.ButtonHTMLAttributes) => {children} ,
+ SignInButton: ({ children, ...props }: React.ButtonHTMLAttributes) => (
+ {children}
+ ),
}));
vi.mock("../../components/ui/card", () => ({
diff --git a/src/routes/cli/auth.tsx b/src/routes/cli/auth.tsx
index 88e0f466..c22eefa2 100644
--- a/src/routes/cli/auth.tsx
+++ b/src/routes/cli/auth.tsx
@@ -18,7 +18,9 @@ type CliAuthProps = {
navigate?: (url: string) => void;
};
-export function CliAuth({ navigate = (url: string) => window.location.assign(url) }: CliAuthProps = {}) {
+export function CliAuth({
+ navigate = (url: string) => window.location.assign(url),
+}: CliAuthProps = {}) {
const { isAuthenticated, isLoading, me } = useAuthStatus();
const { error: authError, clear: clearAuthError } = useAuthError();
const createToken = useMutation(api.tokens.create);
@@ -79,7 +81,17 @@ export function CliAuth({ navigate = (url: string) => window.location.assign(url
setStatus(message);
setToken(null);
});
- }, [createToken, isAuthenticated, label, me, navigate, redirectUri, registry, safeRedirect, state]);
+ }, [
+ createToken,
+ isAuthenticated,
+ label,
+ me,
+ navigate,
+ redirectUri,
+ registry,
+ safeRedirect,
+ state,
+ ]);
if (!safeRedirect) {
return (
@@ -169,9 +181,9 @@ export function CliAuth({ navigate = (url: string) => window.location.assign(url
{token ? (
- If the redirect did not complete, copy this token and run{" "}
- clawhub login --token <token>:
-
+ If the redirect did not complete, copy this token and run{" "}
+
clawhub login --token <token>:
+
{token}
{callbackUrl ? (
diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx
index 451847d7..62dc9a01 100644
--- a/src/routes/dashboard.tsx
+++ b/src/routes/dashboard.tsx
@@ -228,9 +228,7 @@ export function Dashboard() {
Dashboard
-
- View your published skills and plugins.
-
+
View your published skills and plugins.
@@ -306,11 +304,7 @@ function SkillRow({ skill, ownerHandle }: { skill: DashboardSkill; ownerHandle:
-
+
{skill.displayName}
diff --git a/src/routes/import.tsx b/src/routes/import.tsx
index 1310719c..658ef2ae 100644
--- a/src/routes/import.tsx
+++ b/src/routes/import.tsx
@@ -226,9 +226,7 @@ export function ImportGitHub() {
title={isLoading ? "Loading..." : "Sign in to import and publish skills"}
description="You need to be signed in to import skills from GitHub."
>
- {!isLoading ? (
-
- ) : null}
+ {!isLoading ?
: null}
diff --git a/src/routes/plugins/$name/security/$scanner.tsx b/src/routes/plugins/$name/security/$scanner.tsx
index a61ce938..5fd8b716 100644
--- a/src/routes/plugins/$name/security/$scanner.tsx
+++ b/src/routes/plugins/$name/security/$scanner.tsx
@@ -1,8 +1,5 @@
import { createFileRoute, notFound } from "@tanstack/react-router";
-import {
- SecurityScannerPage,
- type ScannerSlug,
-} from "../../../../components/SecurityScannerPage";
+import { SecurityScannerPage, type ScannerSlug } from "../../../../components/SecurityScannerPage";
import {
fetchPackageDetail,
fetchPackageVersion,
diff --git a/src/routes/skills/-SkillsResults.tsx b/src/routes/skills/-SkillsResults.tsx
index 86b84e1d..eb2af216 100644
--- a/src/routes/skills/-SkillsResults.tsx
+++ b/src/routes/skills/-SkillsResults.tsx
@@ -1,7 +1,7 @@
import type { RefObject } from "react";
import { SkillCard } from "../../components/SkillCard";
-import { SkillListItem } from "../../components/SkillListItem";
import { getPlatformLabels } from "../../components/skillDetailUtils";
+import { SkillListItem } from "../../components/SkillListItem";
import { SkillStatsTripletLine } from "../../components/SkillStats";
import { Button } from "../../components/ui/button";
import { UserBadge } from "../../components/UserBadge";
@@ -52,7 +52,9 @@ export function SkillsResults({
No skills found
- {hasQuery ? "Try a different search term or remove filters." : "No skills have been published yet."}
+ {hasQuery
+ ? "Try a different search term or remove filters."
+ : "No skills have been published yet."}
) : view === "cards" ? (
@@ -108,10 +110,7 @@ export function SkillsResults({
)}
{canLoadMore || isLoadingMore ? (
-
+
{canAutoLoad ? (
isLoadingMore ? (
"Loading more..."
diff --git a/src/routes/skills/-SkillsToolbar.tsx b/src/routes/skills/-SkillsToolbar.tsx
index 50fb8c74..0ac586b0 100644
--- a/src/routes/skills/-SkillsToolbar.tsx
+++ b/src/routes/skills/-SkillsToolbar.tsx
@@ -16,7 +16,6 @@ import {
} from "lucide-react";
import type { RefObject } from "react";
import { useMemo } from "react";
-import { SKILL_CATEGORIES, type SkillCategory } from "../../lib/categories";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import {
@@ -26,6 +25,7 @@ import {
SelectTrigger,
SelectValue,
} from "../../components/ui/select";
+import { SKILL_CATEGORIES, type SkillCategory } from "../../lib/categories";
import { type SortDir, type SortKey } from "./-params";
type SkillsToolbarProps = {
@@ -89,9 +89,8 @@ export function SkillsToolbar({
const activeCategory = useMemo(() => {
if (query === "__other__") return "other";
if (!query) return undefined;
- return SKILL_CATEGORIES.find((c) =>
- c.keywords.some((k) => k === query.trim().toLowerCase()),
- )?.slug;
+ return SKILL_CATEGORIES.find((c) => c.keywords.some((k) => k === query.trim().toLowerCase()))
+ ?.slug;
}, [query]);
const handleCategoryChange = (cat: SkillCategory | undefined) => {
diff --git a/src/routes/u/$handle.tsx b/src/routes/u/$handle.tsx
index 3dfc5064..90da073c 100644
--- a/src/routes/u/$handle.tsx
+++ b/src/routes/u/$handle.tsx
@@ -114,9 +114,7 @@ function UserProfile() {
<>
{published.length > 0 ? (
<>
-
- Published ({published.length})
-
+
Published ({published.length})
{isLoadingPublished ? (
Loading published skills...
@@ -131,9 +129,7 @@ function UserProfile() {
>
) : null}
-
- Stars ({skills.length})
-
+ Stars ({skills.length})
{isLoadingSkills ? (
Loading stars...
@@ -164,9 +160,7 @@ function InstalledSection(props: {
if (data === undefined) {
return (
<>
-
- Installed
-
+ Installed
Loading telemetry…
@@ -177,9 +171,7 @@ function InstalledSection(props: {
if (data === null) {
return (
<>
-
- Installed
-
+ Installed
Sign in to view your installed skills.
>
);
@@ -187,9 +179,7 @@ function InstalledSection(props: {
return (
<>
-
- Installed
-
+ Installed
Private view. Only you can see your folders/roots. Everyone else only sees aggregated
install counts per skill.
@@ -214,9 +204,7 @@ function InstalledSection(props: {
{showRaw ? (
-
- {JSON.stringify(data, null, 2)}
-
+ {JSON.stringify(data, null, 2)}
) : null}
diff --git a/vite.config.ts b/vite.config.ts
index 1a571fa4..c61a11e3 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -41,7 +41,8 @@ function handleRollupWarning(
type SourceReplacement = readonly [from: string, to: string];
-const reflectHas = (target: string, key: string) => `Reflect.has(${target}, ${JSON.stringify(key)})`;
+const reflectHas = (target: string, key: string) =>
+ `Reflect.has(${target}, ${JSON.stringify(key)})`;
const arkSafariInOperatorFixes = [
{
@@ -89,15 +90,21 @@ const arkSafariInOperatorFixes = [
},
{
suffix: "/node_modules/@ark/schema/out/node.js",
- replacements: [['"value" in transformedInner', reflectHas("transformedInner", "value")]] satisfies SourceReplacement[],
+ replacements: [
+ ['"value" in transformedInner', reflectHas("transformedInner", "value")],
+ ] satisfies SourceReplacement[],
},
{
suffix: "/node_modules/@ark/schema/out/scope.js",
- replacements: [['"branches" in schema', reflectHas("schema", "branches")]] satisfies SourceReplacement[],
+ replacements: [
+ ['"branches" in schema', reflectHas("schema", "branches")],
+ ] satisfies SourceReplacement[],
},
{
suffix: "/node_modules/@ark/schema/out/structure/optional.js",
- replacements: [['"default" in this.inner', reflectHas("this.inner", "default")]] satisfies SourceReplacement[],
+ replacements: [
+ ['"default" in this.inner', reflectHas("this.inner", "default")],
+ ] satisfies SourceReplacement[],
},
{
suffix: "/node_modules/@ark/schema/out/structure/sequence.js",
@@ -112,11 +119,15 @@ const arkSafariInOperatorFixes = [
},
{
suffix: "/node_modules/@ark/schema/out/structure/prop.js",
- replacements: [['"default" in this.inner', reflectHas("this.inner", "default")]] satisfies SourceReplacement[],
+ replacements: [
+ ['"default" in this.inner', reflectHas("this.inner", "default")],
+ ] satisfies SourceReplacement[],
},
{
suffix: "/node_modules/@ark/schema/out/shared/implement.js",
- replacements: [['"description" in ctx', reflectHas("ctx", "description")]] satisfies SourceReplacement[],
+ replacements: [
+ ['"description" in ctx', reflectHas("ctx", "description")],
+ ] satisfies SourceReplacement[],
},
{
suffix: "/node_modules/@ark/schema/out/shared/errors.js",