clawnet 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "clawnet",
3
+ "version": "0.0.1",
4
+ "description": "CLI for clawhub.network - install, publish, and search agent skills on-chain",
5
+ "type": "module",
6
+ "bin": {
7
+ "clawnet": "./bin/clawnet.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/"
12
+ ],
13
+ "scripts": {
14
+ "build": "bun build ./src/cli.ts --target=bun --outfile=bin/clawnet.js",
15
+ "prepublishOnly": "bun run build",
16
+ "lint": "biome check src/",
17
+ "lint:fix": "biome check --write src/"
18
+ },
19
+ "dependencies": {},
20
+ "devDependencies": {
21
+ "@clawhub/sdk": "0.1.0",
22
+ "@types/node": "^20",
23
+ "chalk": "^5.4.1",
24
+ "typescript": "^5"
25
+ },
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/b-open-io/clawhub.network",
30
+ "directory": "packages/cli"
31
+ },
32
+ "keywords": ["ai", "agent", "skills", "bsv", "blockchain", "cli"]
33
+ }
package/src/api.ts ADDED
@@ -0,0 +1,162 @@
1
+ import { DEFAULT_REGISTRY } from "./config.js";
2
+
3
+ export class ApiError extends Error {
4
+ constructor(
5
+ public status: number,
6
+ message: string
7
+ ) {
8
+ super(message);
9
+ this.name = "ApiError";
10
+ }
11
+ }
12
+
13
+ async function request(
14
+ path: string,
15
+ options: RequestInit = {},
16
+ token?: string
17
+ ): Promise<unknown> {
18
+ const url = `${DEFAULT_REGISTRY}/api/v1${path}`;
19
+ const headers: Record<string, string> = {
20
+ ...(options.headers as Record<string, string>),
21
+ };
22
+
23
+ if (token) {
24
+ headers.Authorization = `Bearer ${token}`;
25
+ }
26
+
27
+ if (options.body && !headers["Content-Type"]) {
28
+ headers["Content-Type"] = "application/json";
29
+ }
30
+
31
+ const res = await fetch(url, { ...options, headers });
32
+
33
+ if (!res.ok) {
34
+ if (res.status === 401) {
35
+ throw new ApiError(401, 'Not authenticated. Run "clawnet login" first.');
36
+ }
37
+ const body = await res.json().catch(() => ({ error: res.statusText }));
38
+ throw new ApiError(
39
+ res.status,
40
+ (body as { error?: string }).error || res.statusText
41
+ );
42
+ }
43
+
44
+ return res.json();
45
+ }
46
+
47
+ export interface SkillSummary {
48
+ slug: string;
49
+ name: string;
50
+ description: string;
51
+ authorBapId: string;
52
+ latestVersion: string;
53
+ latestTxId: string;
54
+ homepage?: string;
55
+ tags?: string[];
56
+ starCount: number;
57
+ downloadCount: number;
58
+ createdAt: number;
59
+ updatedAt: number;
60
+ }
61
+
62
+ export interface SkillDetail {
63
+ skill: SkillSummary & { deleted: boolean; downloadCountAllTime: number };
64
+ latestVersion: {
65
+ version: string;
66
+ txId: string;
67
+ content: string;
68
+ contentType: string;
69
+ changelog?: string;
70
+ files: { path: string; content: string }[];
71
+ publishedAt: number;
72
+ onChain: boolean;
73
+ } | null;
74
+ author: { bapId: string; pubkey: string } | null;
75
+ }
76
+
77
+ export interface SearchResult {
78
+ slug: string;
79
+ name: string;
80
+ description: string;
81
+ authorBapId: string;
82
+ latestVersion: string;
83
+ score: number;
84
+ }
85
+
86
+ export interface WhoamiResponse {
87
+ user: { bapId: string; pubkey: string };
88
+ }
89
+
90
+ export async function listSkills(
91
+ sort = "updated",
92
+ limit = 50,
93
+ cursor?: string
94
+ ): Promise<{ skills: SkillSummary[]; hasMore: boolean; cursor?: string }> {
95
+ const params = new URLSearchParams({ sort, limit: String(limit) });
96
+ if (cursor) params.set("cursor", cursor);
97
+ return request(`/skills?${params}`) as Promise<{
98
+ skills: SkillSummary[];
99
+ hasMore: boolean;
100
+ cursor?: string;
101
+ }>;
102
+ }
103
+
104
+ export async function getSkill(slug: string): Promise<SkillDetail> {
105
+ return request(`/skills/${encodeURIComponent(slug)}`) as Promise<SkillDetail>;
106
+ }
107
+
108
+ export async function searchSkills(
109
+ q: string,
110
+ limit = 20
111
+ ): Promise<{ results: SearchResult[] }> {
112
+ const params = new URLSearchParams({ q, limit: String(limit) });
113
+ return request(`/search?${params}`) as Promise<{
114
+ results: SearchResult[];
115
+ }>;
116
+ }
117
+
118
+ export async function publishSkill(
119
+ payload: {
120
+ slug: string;
121
+ name: string;
122
+ description: string;
123
+ version: string;
124
+ changelog?: string;
125
+ tags?: string[];
126
+ homepage?: string;
127
+ files: { path: string; content: string }[];
128
+ },
129
+ token: string
130
+ ): Promise<{ ok: boolean; slug: string; version: string }> {
131
+ return request(
132
+ "/skills",
133
+ { method: "POST", body: JSON.stringify(payload) },
134
+ token
135
+ ) as Promise<{ ok: boolean; slug: string; version: string }>;
136
+ }
137
+
138
+ export async function fetchWhoami(token: string): Promise<WhoamiResponse> {
139
+ return request("/whoami", {}, token) as Promise<WhoamiResponse>;
140
+ }
141
+
142
+ export async function starSkill(
143
+ slug: string,
144
+ token: string
145
+ ): Promise<{ ok: boolean }> {
146
+ return request(
147
+ `/stars/${encodeURIComponent(slug)}`,
148
+ { method: "POST" },
149
+ token
150
+ ) as Promise<{ ok: boolean }>;
151
+ }
152
+
153
+ export async function unstarSkill(
154
+ slug: string,
155
+ token: string
156
+ ): Promise<{ ok: boolean }> {
157
+ return request(
158
+ `/stars/${encodeURIComponent(slug)}`,
159
+ { method: "DELETE" },
160
+ token
161
+ ) as Promise<{ ok: boolean }>;
162
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+
3
+ import { ensureConfigDir, TOKEN_PATH } from "./config.js";
4
+ import { decrypt, encrypt } from "./crypt.js";
5
+
6
+ export async function getSavedToken(): Promise<string | null> {
7
+ if (!existsSync(TOKEN_PATH)) return null;
8
+
9
+ const encrypted = readFileSync(TOKEN_PATH, "utf8").trim();
10
+ if (!encrypted) return null;
11
+
12
+ return decrypt(encrypted);
13
+ }
14
+
15
+ export async function saveToken(token: string): Promise<void> {
16
+ ensureConfigDir();
17
+ const encrypted = await encrypt(token);
18
+ writeFileSync(TOKEN_PATH, encrypted, { mode: 0o600 });
19
+ }
20
+
21
+ export async function clearToken(): Promise<void> {
22
+ if (existsSync(TOKEN_PATH)) {
23
+ writeFileSync(TOKEN_PATH, "", "utf8");
24
+ }
25
+ }
26
+
27
+ export async function requireToken(): Promise<string> {
28
+ const token = await getSavedToken();
29
+ if (!token) {
30
+ throw new Error('Not authenticated. Run "clawnet login" first.');
31
+ }
32
+ return token;
33
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { info } from "./commands/info.js";
4
+ import { install } from "./commands/install.js";
5
+ import { login } from "./commands/login.js";
6
+ import { logout } from "./commands/logout.js";
7
+ import { publish } from "./commands/publish.js";
8
+ import { search } from "./commands/search.js";
9
+ import { star, unstar } from "./commands/star.js";
10
+ import { whoami } from "./commands/whoami.js";
11
+
12
+ const args = process.argv.slice(2);
13
+ const command = args[0];
14
+ const rest = args.slice(1);
15
+
16
+ function printHelp() {
17
+ console.log(`
18
+ clawnet - On-chain skill registry for AI agents
19
+
20
+ Usage:
21
+ clawnet <command> [options]
22
+
23
+ Commands:
24
+ login Authenticate with Sigma Auth
25
+ logout Clear saved credentials
26
+ whoami Show current identity
27
+
28
+ publish [path] Publish a skill from a directory (default: .)
29
+ install <slug> Install a skill to agent skill directories
30
+ search <query> Search for skills
31
+ info <slug> Show skill details
32
+
33
+ star <slug> Star a skill
34
+ unstar <slug> Unstar a skill
35
+
36
+ Options:
37
+ --help, -h Show this help message
38
+ --version, -v Show version
39
+
40
+ Registry: https://clawhub.network
41
+ `);
42
+ }
43
+
44
+ function printVersion() {
45
+ console.log("clawnet 0.0.1");
46
+ }
47
+
48
+ async function main() {
49
+ if (
50
+ !command ||
51
+ command === "help" ||
52
+ command === "--help" ||
53
+ command === "-h"
54
+ ) {
55
+ printHelp();
56
+ process.exit(0);
57
+ }
58
+
59
+ if (command === "--version" || command === "-v") {
60
+ printVersion();
61
+ process.exit(0);
62
+ }
63
+
64
+ switch (command) {
65
+ case "login":
66
+ await login();
67
+ break;
68
+
69
+ case "logout":
70
+ await logout();
71
+ break;
72
+
73
+ case "whoami":
74
+ await whoami();
75
+ break;
76
+
77
+ case "publish":
78
+ await publish(rest[0]);
79
+ break;
80
+
81
+ case "install": {
82
+ const slug = rest[0];
83
+ if (!slug) {
84
+ console.error("Error: Missing slug argument");
85
+ console.error("Usage: clawnet install <slug>");
86
+ process.exit(1);
87
+ }
88
+ const version = rest.includes("--version")
89
+ ? rest[rest.indexOf("--version") + 1]
90
+ : undefined;
91
+ await install(slug, version);
92
+ break;
93
+ }
94
+
95
+ case "search": {
96
+ const query = rest.join(" ");
97
+ if (!query) {
98
+ console.error("Error: Missing search query");
99
+ console.error("Usage: clawnet search <query>");
100
+ process.exit(1);
101
+ }
102
+ await search(query);
103
+ break;
104
+ }
105
+
106
+ case "info": {
107
+ const slug = rest[0];
108
+ if (!slug) {
109
+ console.error("Error: Missing slug argument");
110
+ console.error("Usage: clawnet info <slug>");
111
+ process.exit(1);
112
+ }
113
+ await info(slug);
114
+ break;
115
+ }
116
+
117
+ case "star": {
118
+ const slug = rest[0];
119
+ if (!slug) {
120
+ console.error("Error: Missing slug argument");
121
+ console.error("Usage: clawnet star <slug>");
122
+ process.exit(1);
123
+ }
124
+ await star(slug);
125
+ break;
126
+ }
127
+
128
+ case "unstar": {
129
+ const slug = rest[0];
130
+ if (!slug) {
131
+ console.error("Error: Missing slug argument");
132
+ console.error("Usage: clawnet unstar <slug>");
133
+ process.exit(1);
134
+ }
135
+ await unstar(slug);
136
+ break;
137
+ }
138
+
139
+ default:
140
+ console.error(`Unknown command: ${command}`);
141
+ printHelp();
142
+ process.exit(1);
143
+ }
144
+ }
145
+
146
+ main().catch((err) => {
147
+ console.error("Error:", err.message);
148
+ process.exit(1);
149
+ });
@@ -0,0 +1,14 @@
1
+ import { getSkill } from "../api.js";
2
+ import { formatError, formatSkillDetail } from "../format.js";
3
+
4
+ export async function info(slug: string): Promise<void> {
5
+ try {
6
+ const data = await getSkill(slug);
7
+ console.log(formatSkillDetail(data));
8
+ } catch (err) {
9
+ console.error(
10
+ formatError(err instanceof Error ? err.message : String(err))
11
+ );
12
+ process.exit(1);
13
+ }
14
+ }
@@ -0,0 +1,66 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import { getSkill } from "../api.js";
5
+ import { formatError, formatSuccess } from "../format.js";
6
+
7
+ const SKILL_DIRS = [".claude/skills", ".cursor/skills", ".gemini/skills"];
8
+
9
+ function detectTargetDir(): string {
10
+ for (const dir of SKILL_DIRS) {
11
+ if (existsSync(dir)) {
12
+ return dir;
13
+ }
14
+ }
15
+ return ".claude/skills";
16
+ }
17
+
18
+ export async function install(slug: string, version?: string): Promise<void> {
19
+ try {
20
+ const data = await getSkill(slug);
21
+
22
+ if (!data.latestVersion) {
23
+ console.error(formatError(`No published version found for ${slug}.`));
24
+ process.exit(1);
25
+ }
26
+
27
+ if (version && data.latestVersion.version !== version) {
28
+ console.error(
29
+ formatError(
30
+ `Version ${version} not found. Latest is ${data.latestVersion.version}.`
31
+ )
32
+ );
33
+ process.exit(1);
34
+ }
35
+
36
+ const targetDir = detectTargetDir();
37
+ const skillDir = join(targetDir, slug);
38
+ mkdirSync(skillDir, { recursive: true });
39
+
40
+ if (data.latestVersion.files && data.latestVersion.files.length > 0) {
41
+ for (const file of data.latestVersion.files) {
42
+ const filePath = join(skillDir, file.path);
43
+ const fileDir = join(filePath, "..");
44
+ mkdirSync(fileDir, { recursive: true });
45
+ writeFileSync(filePath, file.content, "utf8");
46
+ }
47
+ } else if (data.latestVersion.content) {
48
+ writeFileSync(
49
+ join(skillDir, "SKILL.md"),
50
+ data.latestVersion.content,
51
+ "utf8"
52
+ );
53
+ }
54
+
55
+ console.log(
56
+ formatSuccess(
57
+ `Installed ${slug}@${data.latestVersion.version} to ${skillDir}`
58
+ )
59
+ );
60
+ } catch (err) {
61
+ console.error(
62
+ formatError(err instanceof Error ? err.message : String(err))
63
+ );
64
+ process.exit(1);
65
+ }
66
+ }
@@ -0,0 +1,116 @@
1
+ import { execSync } from "node:child_process";
2
+ import { saveToken } from "../auth.js";
3
+ import { SIGMA_AUTH_URL, SIGMA_CLIENT_ID } from "../config.js";
4
+ import { formatError, formatSuccess } from "../format.js";
5
+
6
+ function openBrowser(url: string): void {
7
+ const cmd =
8
+ process.platform === "darwin"
9
+ ? "open"
10
+ : process.platform === "win32"
11
+ ? "start"
12
+ : "xdg-open";
13
+
14
+ try {
15
+ execSync(`${cmd} "${url}"`, { stdio: "ignore" });
16
+ } catch {
17
+ console.log(`Open this URL in your browser:\n ${url}`);
18
+ }
19
+ }
20
+
21
+ interface DeviceResponse {
22
+ device_code: string;
23
+ user_code: string;
24
+ verification_uri: string;
25
+ verification_uri_complete?: string;
26
+ expires_in: number;
27
+ interval: number;
28
+ }
29
+
30
+ interface TokenResponse {
31
+ access_token: string;
32
+ token_type: string;
33
+ expires_in?: number;
34
+ error?: string;
35
+ error_description?: string;
36
+ }
37
+
38
+ export async function login(): Promise<void> {
39
+ console.log("Starting Sigma Auth device flow...\n");
40
+
41
+ const deviceRes = await fetch(`${SIGMA_AUTH_URL}/api/auth/device/code`, {
42
+ method: "POST",
43
+ headers: { "Content-Type": "application/json" },
44
+ body: JSON.stringify({
45
+ client_id: SIGMA_CLIENT_ID,
46
+ scope: "openid profile",
47
+ }),
48
+ });
49
+
50
+ if (!deviceRes.ok) {
51
+ const text = await deviceRes.text();
52
+ console.error(formatError(`Device authorization failed: ${text}`));
53
+ process.exit(1);
54
+ }
55
+
56
+ const device: DeviceResponse = await deviceRes.json();
57
+ const verifyUrl =
58
+ device.verification_uri_complete ||
59
+ `${device.verification_uri}?user_code=${device.user_code}`;
60
+
61
+ console.log(`Go to: ${device.verification_uri}`);
62
+ console.log(`Enter code: ${device.user_code}\n`);
63
+
64
+ openBrowser(verifyUrl);
65
+
66
+ console.log("Waiting for authorization...");
67
+
68
+ let pollInterval = (device.interval || 5) * 1000;
69
+ const deadline = Date.now() + device.expires_in * 1000;
70
+
71
+ while (Date.now() < deadline) {
72
+ await new Promise((r) => setTimeout(r, pollInterval));
73
+
74
+ const tokenRes = await fetch(`${SIGMA_AUTH_URL}/api/auth/device/token`, {
75
+ method: "POST",
76
+ headers: { "Content-Type": "application/json" },
77
+ body: JSON.stringify({
78
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
79
+ device_code: device.device_code,
80
+ client_id: SIGMA_CLIENT_ID,
81
+ }),
82
+ });
83
+
84
+ const tokenData: TokenResponse = await tokenRes.json();
85
+
86
+ if (tokenData.access_token) {
87
+ await saveToken(tokenData.access_token);
88
+ console.log(formatSuccess("Logged in."));
89
+ return;
90
+ }
91
+
92
+ if (tokenData.error === "authorization_pending") {
93
+ continue;
94
+ }
95
+
96
+ if (tokenData.error === "slow_down") {
97
+ pollInterval += 5000;
98
+ continue;
99
+ }
100
+
101
+ if (tokenData.error === "expired_token") {
102
+ console.error(formatError("Authorization expired. Please try again."));
103
+ process.exit(1);
104
+ }
105
+
106
+ if (tokenData.error) {
107
+ console.error(
108
+ formatError(tokenData.error_description || tokenData.error)
109
+ );
110
+ process.exit(1);
111
+ }
112
+ }
113
+
114
+ console.error(formatError("Authorization timed out. Please try again."));
115
+ process.exit(1);
116
+ }
@@ -0,0 +1,6 @@
1
+ import { clearToken } from "../auth.js";
2
+
3
+ export async function logout(): Promise<void> {
4
+ await clearToken();
5
+ console.log("Logged out.");
6
+ }
@@ -0,0 +1,126 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { isAbsolute, join, relative } from "node:path";
3
+
4
+ import { parseSkillFrontmatter, validateSkillContent } from "@clawhub/sdk";
5
+ import { publishSkill } from "../api.js";
6
+ import { requireToken } from "../auth.js";
7
+ import { formatError, formatSuccess } from "../format.js";
8
+
9
+ const SKIP_DIRS = new Set(["node_modules", ".git", ".DS_Store", "__pycache__"]);
10
+ const BINARY_EXTS = new Set([
11
+ ".png",
12
+ ".jpg",
13
+ ".jpeg",
14
+ ".gif",
15
+ ".ico",
16
+ ".woff",
17
+ ".woff2",
18
+ ".ttf",
19
+ ".eot",
20
+ ".zip",
21
+ ".tar",
22
+ ".gz",
23
+ ".exe",
24
+ ".bin",
25
+ ]);
26
+
27
+ function collectFiles(
28
+ dir: string,
29
+ base: string
30
+ ): { path: string; content: string }[] {
31
+ const files: { path: string; content: string }[] = [];
32
+
33
+ for (const entry of readdirSync(dir)) {
34
+ if (SKIP_DIRS.has(entry)) continue;
35
+ if (entry.startsWith(".")) continue;
36
+
37
+ const fullPath = join(dir, entry);
38
+ const stat = statSync(fullPath);
39
+
40
+ if (stat.isDirectory()) {
41
+ files.push(...collectFiles(fullPath, base));
42
+ } else if (stat.isFile()) {
43
+ const ext = entry.slice(entry.lastIndexOf(".")).toLowerCase();
44
+ if (BINARY_EXTS.has(ext)) continue;
45
+ if (stat.size > 1024 * 1024) continue;
46
+
47
+ const relPath = relative(base, fullPath);
48
+ const content = readFileSync(fullPath, "utf8");
49
+ files.push({ path: relPath, content });
50
+ }
51
+ }
52
+
53
+ return files;
54
+ }
55
+
56
+ export async function publish(path?: string): Promise<void> {
57
+ const token = await requireToken();
58
+ const dir = path
59
+ ? isAbsolute(path)
60
+ ? path
61
+ : join(process.cwd(), path)
62
+ : process.cwd();
63
+
64
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) {
65
+ console.error(formatError(`Not a directory: ${dir}`));
66
+ process.exit(1);
67
+ }
68
+
69
+ const skillMdPath = existsSync(join(dir, "SKILL.md"))
70
+ ? join(dir, "SKILL.md")
71
+ : existsSync(join(dir, "skill.md"))
72
+ ? join(dir, "skill.md")
73
+ : null;
74
+
75
+ if (!skillMdPath) {
76
+ console.error(formatError("No SKILL.md found in directory."));
77
+ process.exit(1);
78
+ }
79
+
80
+ const skillContent = readFileSync(skillMdPath, "utf8");
81
+ const validation = validateSkillContent(skillContent);
82
+
83
+ if (!validation.valid) {
84
+ console.error(formatError("Invalid SKILL.md:"));
85
+ for (const e of validation.errors) {
86
+ console.error(` - ${e}`);
87
+ }
88
+ process.exit(1);
89
+ }
90
+
91
+ const frontmatter = parseSkillFrontmatter(skillContent);
92
+ if (!frontmatter.success) {
93
+ console.error(
94
+ formatError(`Failed to parse frontmatter: ${frontmatter.error}`)
95
+ );
96
+ process.exit(1);
97
+ }
98
+
99
+ const { name, description, version } = frontmatter.data;
100
+ const slug = name;
101
+
102
+ console.log(`Publishing ${slug}@${version}...`);
103
+
104
+ const files = collectFiles(dir, dir);
105
+
106
+ try {
107
+ const result = await publishSkill(
108
+ {
109
+ slug,
110
+ name,
111
+ description,
112
+ version,
113
+ tags: frontmatter.data.tags,
114
+ homepage: frontmatter.data.homepage,
115
+ files,
116
+ },
117
+ token
118
+ );
119
+ console.log(formatSuccess(`Published ${result.slug}@${result.version}`));
120
+ } catch (err) {
121
+ console.error(
122
+ formatError(err instanceof Error ? err.message : String(err))
123
+ );
124
+ process.exit(1);
125
+ }
126
+ }