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/bin/clawnet.js +14714 -0
- package/package.json +33 -0
- package/src/api.ts +162 -0
- package/src/auth.ts +33 -0
- package/src/cli.ts +149 -0
- package/src/commands/info.ts +14 -0
- package/src/commands/install.ts +66 -0
- package/src/commands/login.ts +116 -0
- package/src/commands/logout.ts +6 -0
- package/src/commands/publish.ts +126 -0
- package/src/commands/search.ts +23 -0
- package/src/commands/star.ts +31 -0
- package/src/commands/whoami.ts +18 -0
- package/src/config.ts +18 -0
- package/src/crypt.ts +60 -0
- package/src/format.ts +68 -0
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,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
|
+
}
|