askill-cli 0.1.2 → 0.1.3
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/README.md +11 -1
- package/dist/cli.mjs +438 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,6 +24,13 @@ askill list
|
|
|
24
24
|
# Search for skills
|
|
25
25
|
askill find <query>
|
|
26
26
|
|
|
27
|
+
# Submit a GitHub skill URL for indexing
|
|
28
|
+
askill submit https://github.com/owner/repo
|
|
29
|
+
|
|
30
|
+
# Login and publish your own skill
|
|
31
|
+
askill login --token ask_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
32
|
+
askill publish
|
|
33
|
+
|
|
27
34
|
# Run a skill command
|
|
28
35
|
askill run skill-name:command
|
|
29
36
|
```
|
|
@@ -50,6 +57,8 @@ After installation, read the skill's `SKILL.md` file for usage instructions. Ski
|
|
|
50
57
|
|
|
51
58
|
askill is a universal package manager for AI agent skills. It enables agents to discover, install, and use skills across Claude Code, Cursor, Windsurf, and 40+ other AI coding assistants.
|
|
52
59
|
|
|
60
|
+
Every skill on [askill.sh](https://askill.sh) is automatically reviewed by AI across 5 quality dimensions — safety, clarity, reusability, completeness, and actionability — so you can evaluate quality before you install.
|
|
61
|
+
|
|
53
62
|
## Quick Start
|
|
54
63
|
|
|
55
64
|
```bash
|
|
@@ -175,7 +184,8 @@ Instructions for the agent...
|
|
|
175
184
|
|
|
176
185
|
2. Test locally: `askill add ./my-skill`
|
|
177
186
|
3. Validate: `askill validate`
|
|
178
|
-
4.
|
|
187
|
+
4. Submit for indexing: `askill submit https://github.com/<owner>/<repo>`
|
|
188
|
+
5. Publish under your author scope: `askill login` then `askill publish`
|
|
179
189
|
|
|
180
190
|
See [Publishing Guide](./docs/publishing.md) for details.
|
|
181
191
|
|
package/dist/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { existsSync } from "fs";
|
|
7
|
-
var VERSION = "0.1.
|
|
7
|
+
var VERSION = "0.1.3";
|
|
8
8
|
var API_BASE_URL = "https://askill.sh/api/v1";
|
|
9
9
|
var REGISTRY_URL = "https://askill.sh";
|
|
10
10
|
var RESET = "\x1B[0m";
|
|
@@ -396,6 +396,40 @@ var APIClient = class {
|
|
|
396
396
|
async checkCLIVersion() {
|
|
397
397
|
return this.fetch("/cli/version");
|
|
398
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* Submit GitHub URL for indexing
|
|
401
|
+
*/
|
|
402
|
+
async submit(url) {
|
|
403
|
+
return this.fetch("/submit", {
|
|
404
|
+
method: "POST",
|
|
405
|
+
body: JSON.stringify({ url })
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Verify API token and fetch user profile
|
|
410
|
+
*/
|
|
411
|
+
async authMe(token) {
|
|
412
|
+
return this.fetch("/auth/me", {
|
|
413
|
+
headers: {
|
|
414
|
+
Authorization: `Bearer ${token}`
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Publish a skill from local content or GitHub URL
|
|
420
|
+
*/
|
|
421
|
+
async publish(payload) {
|
|
422
|
+
return this.fetch("/publish", {
|
|
423
|
+
method: "POST",
|
|
424
|
+
headers: {
|
|
425
|
+
Authorization: `Bearer ${payload.token}`
|
|
426
|
+
},
|
|
427
|
+
body: JSON.stringify({
|
|
428
|
+
content: payload.content,
|
|
429
|
+
githubUrl: payload.githubUrl
|
|
430
|
+
})
|
|
431
|
+
});
|
|
432
|
+
}
|
|
399
433
|
};
|
|
400
434
|
var APIError = class extends Error {
|
|
401
435
|
constructor(status, code, message) {
|
|
@@ -695,8 +729,8 @@ function getPlatformKey() {
|
|
|
695
729
|
}
|
|
696
730
|
async function shouldCheckUpdate() {
|
|
697
731
|
try {
|
|
698
|
-
const { readFile:
|
|
699
|
-
const lastCheck = await
|
|
732
|
+
const { readFile: readFile5 } = await import("fs/promises");
|
|
733
|
+
const lastCheck = await readFile5(UPDATE_CHECK_FILE, "utf-8");
|
|
700
734
|
const lastCheckTime = parseInt(lastCheck, 10);
|
|
701
735
|
return Date.now() - lastCheckTime > UPDATE_INTERVAL_MS;
|
|
702
736
|
} catch {
|
|
@@ -705,9 +739,9 @@ async function shouldCheckUpdate() {
|
|
|
705
739
|
}
|
|
706
740
|
async function saveUpdateCheckTime() {
|
|
707
741
|
try {
|
|
708
|
-
const { mkdir:
|
|
709
|
-
await
|
|
710
|
-
await
|
|
742
|
+
const { mkdir: mkdir5, writeFile: writeFile5 } = await import("fs/promises");
|
|
743
|
+
await mkdir5(dirname2(UPDATE_CHECK_FILE), { recursive: true });
|
|
744
|
+
await writeFile5(UPDATE_CHECK_FILE, String(Date.now()), "utf-8");
|
|
711
745
|
} catch {
|
|
712
746
|
}
|
|
713
747
|
}
|
|
@@ -865,6 +899,37 @@ async function getPreferredAgents() {
|
|
|
865
899
|
return config.preferredAgents;
|
|
866
900
|
}
|
|
867
901
|
|
|
902
|
+
// src/credentials.ts
|
|
903
|
+
import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3, rm as rm2 } from "fs/promises";
|
|
904
|
+
import { join as join5 } from "path";
|
|
905
|
+
import { homedir as homedir5 } from "os";
|
|
906
|
+
var ASKILL_DIR = join5(homedir5(), ".askill");
|
|
907
|
+
var CREDENTIALS_FILE = join5(ASKILL_DIR, "credentials.json");
|
|
908
|
+
async function loadCredentials() {
|
|
909
|
+
try {
|
|
910
|
+
const content = await readFile2(CREDENTIALS_FILE, "utf-8");
|
|
911
|
+
const parsed = JSON.parse(content);
|
|
912
|
+
if (!parsed.token) return null;
|
|
913
|
+
return parsed;
|
|
914
|
+
} catch {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
async function saveCredentials(credentials) {
|
|
919
|
+
await mkdir3(ASKILL_DIR, { recursive: true });
|
|
920
|
+
await writeFile3(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), "utf-8");
|
|
921
|
+
}
|
|
922
|
+
async function clearCredentials() {
|
|
923
|
+
try {
|
|
924
|
+
await rm2(CREDENTIALS_FILE);
|
|
925
|
+
} catch {
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
function maskToken(token) {
|
|
929
|
+
if (token.length <= 8) return token;
|
|
930
|
+
return `${token.slice(0, 8)}****`;
|
|
931
|
+
}
|
|
932
|
+
|
|
868
933
|
// src/parser.ts
|
|
869
934
|
function parseSkillMd(content) {
|
|
870
935
|
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
@@ -1127,12 +1192,12 @@ function isLocalPath(input) {
|
|
|
1127
1192
|
}
|
|
1128
1193
|
|
|
1129
1194
|
// src/discover.ts
|
|
1130
|
-
import { readdir as readdir2, readFile as
|
|
1131
|
-
import { join as
|
|
1195
|
+
import { readdir as readdir2, readFile as readFile3, stat } from "fs/promises";
|
|
1196
|
+
import { join as join6 } from "path";
|
|
1132
1197
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "__pycache__"]);
|
|
1133
1198
|
async function hasSkillMd(dir) {
|
|
1134
1199
|
try {
|
|
1135
|
-
const skillPath =
|
|
1200
|
+
const skillPath = join6(dir, "SKILL.md");
|
|
1136
1201
|
const stats = await stat(skillPath);
|
|
1137
1202
|
return stats.isFile();
|
|
1138
1203
|
} catch {
|
|
@@ -1141,8 +1206,8 @@ async function hasSkillMd(dir) {
|
|
|
1141
1206
|
}
|
|
1142
1207
|
async function parseSkillDir(dir) {
|
|
1143
1208
|
try {
|
|
1144
|
-
const skillPath =
|
|
1145
|
-
const content = await
|
|
1209
|
+
const skillPath = join6(dir, "SKILL.md");
|
|
1210
|
+
const content = await readFile3(skillPath, "utf-8");
|
|
1146
1211
|
const parsed = parseSkillMd(content);
|
|
1147
1212
|
if (!parsed.frontmatter.name || !parsed.frontmatter.description) {
|
|
1148
1213
|
return null;
|
|
@@ -1167,7 +1232,7 @@ async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
|
|
|
1167
1232
|
]);
|
|
1168
1233
|
const currentDir = hasSkill ? [dir] : [];
|
|
1169
1234
|
const subDirResults = await Promise.all(
|
|
1170
|
-
entries.filter((entry) => entry.isDirectory() && !SKIP_DIRS.has(entry.name)).map((entry) => findSkillDirs(
|
|
1235
|
+
entries.filter((entry) => entry.isDirectory() && !SKIP_DIRS.has(entry.name)).map((entry) => findSkillDirs(join6(dir, entry.name), depth + 1, maxDepth))
|
|
1171
1236
|
);
|
|
1172
1237
|
return [...currentDir, ...subDirResults.flat()];
|
|
1173
1238
|
} catch {
|
|
@@ -1177,7 +1242,7 @@ async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
|
|
|
1177
1242
|
async function discoverSkills(basePath, subpath) {
|
|
1178
1243
|
const skills = [];
|
|
1179
1244
|
const seenNames = /* @__PURE__ */ new Set();
|
|
1180
|
-
const searchPath = subpath ?
|
|
1245
|
+
const searchPath = subpath ? join6(basePath, subpath) : basePath;
|
|
1181
1246
|
if (await hasSkillMd(searchPath)) {
|
|
1182
1247
|
const skill = await parseSkillDir(searchPath);
|
|
1183
1248
|
if (skill) {
|
|
@@ -1186,27 +1251,27 @@ async function discoverSkills(basePath, subpath) {
|
|
|
1186
1251
|
}
|
|
1187
1252
|
const prioritySearchDirs = [
|
|
1188
1253
|
searchPath,
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1254
|
+
join6(searchPath, "skills"),
|
|
1255
|
+
join6(searchPath, "skills/.curated"),
|
|
1256
|
+
join6(searchPath, "skills/.experimental"),
|
|
1257
|
+
join6(searchPath, ".agents/skills"),
|
|
1258
|
+
join6(searchPath, ".claude/skills"),
|
|
1259
|
+
join6(searchPath, ".opencode/skills"),
|
|
1260
|
+
join6(searchPath, ".cursor/skills"),
|
|
1261
|
+
join6(searchPath, ".codex/skills"),
|
|
1262
|
+
join6(searchPath, ".cline/skills"),
|
|
1263
|
+
join6(searchPath, ".gemini/skills"),
|
|
1264
|
+
join6(searchPath, ".windsurf/skills"),
|
|
1265
|
+
join6(searchPath, ".roo/skills"),
|
|
1266
|
+
join6(searchPath, ".github/skills"),
|
|
1267
|
+
join6(searchPath, ".goose/skills")
|
|
1203
1268
|
];
|
|
1204
1269
|
for (const dir of prioritySearchDirs) {
|
|
1205
1270
|
try {
|
|
1206
1271
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
1207
1272
|
for (const entry of entries) {
|
|
1208
1273
|
if (entry.isDirectory()) {
|
|
1209
|
-
const skillDir =
|
|
1274
|
+
const skillDir = join6(dir, entry.name);
|
|
1210
1275
|
if (await hasSkillMd(skillDir)) {
|
|
1211
1276
|
const skill = await parseSkillDir(skillDir);
|
|
1212
1277
|
if (skill && !seenNames.has(skill.name)) {
|
|
@@ -1241,8 +1306,8 @@ function filterSkills(skills, names) {
|
|
|
1241
1306
|
|
|
1242
1307
|
// src/git.ts
|
|
1243
1308
|
import { execFile } from "child_process";
|
|
1244
|
-
import { mkdtemp, rm as
|
|
1245
|
-
import { join as
|
|
1309
|
+
import { mkdtemp, rm as rm3 } from "fs/promises";
|
|
1310
|
+
import { join as join7, normalize as normalize2, resolve as resolve3, sep as sep2 } from "path";
|
|
1246
1311
|
import { tmpdir } from "os";
|
|
1247
1312
|
var CLONE_TIMEOUT_MS = 6e4;
|
|
1248
1313
|
var GitCloneError = class extends Error {
|
|
@@ -1258,7 +1323,7 @@ var GitCloneError = class extends Error {
|
|
|
1258
1323
|
}
|
|
1259
1324
|
};
|
|
1260
1325
|
async function cloneRepo(url, ref) {
|
|
1261
|
-
const tempDir = await mkdtemp(
|
|
1326
|
+
const tempDir = await mkdtemp(join7(tmpdir(), "askill-"));
|
|
1262
1327
|
const args = ["clone", "--depth", "1"];
|
|
1263
1328
|
if (ref) {
|
|
1264
1329
|
args.push("--branch", ref);
|
|
@@ -1268,7 +1333,7 @@ async function cloneRepo(url, ref) {
|
|
|
1268
1333
|
await execGit(args);
|
|
1269
1334
|
return tempDir;
|
|
1270
1335
|
} catch (error) {
|
|
1271
|
-
await
|
|
1336
|
+
await rm3(tempDir, { recursive: true, force: true }).catch(() => {
|
|
1272
1337
|
});
|
|
1273
1338
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1274
1339
|
const isTimeout = errorMessage.includes("timed out") || errorMessage.includes("timeout");
|
|
@@ -1301,7 +1366,7 @@ async function cleanupTempDir(dir) {
|
|
|
1301
1366
|
if (!normalizedDir.startsWith(normalizedTmpDir + sep2) && normalizedDir !== normalizedTmpDir) {
|
|
1302
1367
|
throw new Error("Attempted to clean up directory outside of temp directory");
|
|
1303
1368
|
}
|
|
1304
|
-
await
|
|
1369
|
+
await rm3(dir, { recursive: true, force: true });
|
|
1305
1370
|
}
|
|
1306
1371
|
function execGit(args) {
|
|
1307
1372
|
return new Promise((resolve4, reject) => {
|
|
@@ -1316,14 +1381,14 @@ function execGit(args) {
|
|
|
1316
1381
|
}
|
|
1317
1382
|
|
|
1318
1383
|
// src/lock.ts
|
|
1319
|
-
import { readFile as
|
|
1320
|
-
import { join as
|
|
1321
|
-
import { homedir as
|
|
1384
|
+
import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
|
|
1385
|
+
import { join as join8, dirname as dirname5 } from "path";
|
|
1386
|
+
import { homedir as homedir6 } from "os";
|
|
1322
1387
|
var AGENTS_DIR2 = ".agents";
|
|
1323
1388
|
var LOCK_FILE = ".skill-lock.json";
|
|
1324
1389
|
var CURRENT_VERSION = 3;
|
|
1325
1390
|
function getSkillLockPath() {
|
|
1326
|
-
return
|
|
1391
|
+
return join8(homedir6(), AGENTS_DIR2, LOCK_FILE);
|
|
1327
1392
|
}
|
|
1328
1393
|
function createEmptyLockFile() {
|
|
1329
1394
|
return {
|
|
@@ -1334,7 +1399,7 @@ function createEmptyLockFile() {
|
|
|
1334
1399
|
async function readSkillLock() {
|
|
1335
1400
|
const lockPath = getSkillLockPath();
|
|
1336
1401
|
try {
|
|
1337
|
-
const content = await
|
|
1402
|
+
const content = await readFile4(lockPath, "utf-8");
|
|
1338
1403
|
const parsed = JSON.parse(content);
|
|
1339
1404
|
if (typeof parsed.version !== "number" || !parsed.skills) {
|
|
1340
1405
|
return createEmptyLockFile();
|
|
@@ -1349,9 +1414,9 @@ async function readSkillLock() {
|
|
|
1349
1414
|
}
|
|
1350
1415
|
async function writeSkillLock(lock) {
|
|
1351
1416
|
const lockPath = getSkillLockPath();
|
|
1352
|
-
await
|
|
1417
|
+
await mkdir4(dirname5(lockPath), { recursive: true });
|
|
1353
1418
|
const content = JSON.stringify(lock, null, 2);
|
|
1354
|
-
await
|
|
1419
|
+
await writeFile4(lockPath, content, "utf-8");
|
|
1355
1420
|
}
|
|
1356
1421
|
async function addSkillToLock(skillName, entry) {
|
|
1357
1422
|
const lock = await readSkillLock();
|
|
@@ -1425,8 +1490,8 @@ async function fetchSkillFolderHash(ownerRepo, skillPath) {
|
|
|
1425
1490
|
}
|
|
1426
1491
|
|
|
1427
1492
|
// src/cli.ts
|
|
1428
|
-
import { join as
|
|
1429
|
-
import { homedir as
|
|
1493
|
+
import { join as join9 } from "path";
|
|
1494
|
+
import { homedir as homedir7 } from "os";
|
|
1430
1495
|
import * as p from "@clack/prompts";
|
|
1431
1496
|
import pc from "picocolors";
|
|
1432
1497
|
var LOGO = `
|
|
@@ -1455,6 +1520,9 @@ function showBanner() {
|
|
|
1455
1520
|
console.log(` ${DIM}$${RESET} askill list${RESET} ${DIM}List installed skills${RESET}`);
|
|
1456
1521
|
console.log(` ${DIM}$${RESET} askill remove ${DIM}<skill>${RESET} ${DIM}Remove a skill${RESET}`);
|
|
1457
1522
|
console.log(` ${DIM}$${RESET} askill init${RESET} ${DIM}Create a new skill${RESET}`);
|
|
1523
|
+
console.log(` ${DIM}$${RESET} askill submit ${DIM}<url>${RESET} ${DIM}Submit GitHub skill URL${RESET}`);
|
|
1524
|
+
console.log(` ${DIM}$${RESET} askill login${RESET} ${DIM}Login with API token${RESET}`);
|
|
1525
|
+
console.log(` ${DIM}$${RESET} askill publish${RESET} ${DIM}Publish a skill${RESET}`);
|
|
1458
1526
|
console.log(` ${DIM}$${RESET} askill run ${DIM}<skill:cmd>${RESET} ${DIM}Run a skill command${RESET}`);
|
|
1459
1527
|
console.log();
|
|
1460
1528
|
console.log(`${DIM}Browse skills at${RESET} ${CYAN}https://askill.sh${RESET}`);
|
|
@@ -1474,6 +1542,12 @@ ${BOLD}Commands:${RESET}
|
|
|
1474
1542
|
validate [path] Validate a SKILL.md file
|
|
1475
1543
|
check Check installed skills for updates
|
|
1476
1544
|
update [skill] Update installed skills
|
|
1545
|
+
submit <github-url> Submit GitHub URL for indexing
|
|
1546
|
+
login [--token <token>] Login with API token
|
|
1547
|
+
logout Clear saved API token
|
|
1548
|
+
whoami Show current authenticated user
|
|
1549
|
+
publish [path] Publish SKILL.md from local path
|
|
1550
|
+
publish --github <url> Publish SKILL.md from GitHub URL
|
|
1477
1551
|
run <skill:cmd> Run a skill command
|
|
1478
1552
|
upgrade Update askill CLI to latest version
|
|
1479
1553
|
|
|
@@ -1942,6 +2016,118 @@ Formats supported:`);
|
|
|
1942
2016
|
await cleanup();
|
|
1943
2017
|
}
|
|
1944
2018
|
}
|
|
2019
|
+
var SEARCH_DESCRIPTION_MAX_LENGTH = 180;
|
|
2020
|
+
function toNumber(value) {
|
|
2021
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2022
|
+
return null;
|
|
2023
|
+
}
|
|
2024
|
+
return value;
|
|
2025
|
+
}
|
|
2026
|
+
function parseJsonObject(value) {
|
|
2027
|
+
if (typeof value === "string") {
|
|
2028
|
+
try {
|
|
2029
|
+
const parsed = JSON.parse(value);
|
|
2030
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2031
|
+
return parsed;
|
|
2032
|
+
}
|
|
2033
|
+
return null;
|
|
2034
|
+
} catch {
|
|
2035
|
+
return null;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2039
|
+
return value;
|
|
2040
|
+
}
|
|
2041
|
+
return null;
|
|
2042
|
+
}
|
|
2043
|
+
function formatScore(score) {
|
|
2044
|
+
if (score === null) {
|
|
2045
|
+
return pc.dim("N/A");
|
|
2046
|
+
}
|
|
2047
|
+
return pc.green(Number.isInteger(score) ? String(score) : score.toFixed(1));
|
|
2048
|
+
}
|
|
2049
|
+
function toScoreLabel(key) {
|
|
2050
|
+
return key.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
2051
|
+
}
|
|
2052
|
+
function getScoreMeta(skill) {
|
|
2053
|
+
const withScoreMeta = skill;
|
|
2054
|
+
return parseJsonObject(withScoreMeta.llmScoreMeta);
|
|
2055
|
+
}
|
|
2056
|
+
function getTotalAIScore(skill) {
|
|
2057
|
+
const aiScore = toNumber(skill.aiScore);
|
|
2058
|
+
if (aiScore !== null) {
|
|
2059
|
+
return aiScore;
|
|
2060
|
+
}
|
|
2061
|
+
const directScore = toNumber(skill.llmScore);
|
|
2062
|
+
if (directScore !== null) {
|
|
2063
|
+
return directScore;
|
|
2064
|
+
}
|
|
2065
|
+
const meta = getScoreMeta(skill);
|
|
2066
|
+
if (!meta) {
|
|
2067
|
+
return null;
|
|
2068
|
+
}
|
|
2069
|
+
return toNumber(meta.score) ?? toNumber(meta.score_raw) ?? toNumber(meta.final_rank);
|
|
2070
|
+
}
|
|
2071
|
+
function getAIScoreDimensions(skill) {
|
|
2072
|
+
const directBreakdown = parseJsonObject(skill.aiBreakdown);
|
|
2073
|
+
if (directBreakdown) {
|
|
2074
|
+
const parsedDirect = Object.entries(directBreakdown).map(([key, value]) => {
|
|
2075
|
+
const score = toNumber(value);
|
|
2076
|
+
if (score === null) {
|
|
2077
|
+
return null;
|
|
2078
|
+
}
|
|
2079
|
+
return {
|
|
2080
|
+
key,
|
|
2081
|
+
label: toScoreLabel(key),
|
|
2082
|
+
score
|
|
2083
|
+
};
|
|
2084
|
+
}).filter((item) => item !== null);
|
|
2085
|
+
if (parsedDirect.length > 0) {
|
|
2086
|
+
const preferredOrder2 = ["completeness", "actionability", "reusability", "safety", "clarity", "internal_only"];
|
|
2087
|
+
return parsedDirect.sort((a, b) => {
|
|
2088
|
+
const indexA = preferredOrder2.indexOf(a.key);
|
|
2089
|
+
const indexB = preferredOrder2.indexOf(b.key);
|
|
2090
|
+
const rankA = indexA === -1 ? Number.MAX_SAFE_INTEGER : indexA;
|
|
2091
|
+
const rankB = indexB === -1 ? Number.MAX_SAFE_INTEGER : indexB;
|
|
2092
|
+
if (rankA !== rankB) {
|
|
2093
|
+
return rankA - rankB;
|
|
2094
|
+
}
|
|
2095
|
+
return a.label.localeCompare(b.label);
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
const meta = getScoreMeta(skill);
|
|
2100
|
+
if (!meta) {
|
|
2101
|
+
return [];
|
|
2102
|
+
}
|
|
2103
|
+
const dimensions = parseJsonObject(meta.dimensions);
|
|
2104
|
+
if (!dimensions) {
|
|
2105
|
+
return [];
|
|
2106
|
+
}
|
|
2107
|
+
const preferredOrder = ["completeness", "actionability", "reusability", "safety", "clarity", "internal_only"];
|
|
2108
|
+
const parsed = Object.entries(dimensions).map(([key, value]) => {
|
|
2109
|
+
const nested = parseJsonObject(value);
|
|
2110
|
+
const score = nested ? toNumber(nested.score) : toNumber(value);
|
|
2111
|
+
if (score === null) {
|
|
2112
|
+
return null;
|
|
2113
|
+
}
|
|
2114
|
+
return {
|
|
2115
|
+
key,
|
|
2116
|
+
label: toScoreLabel(key),
|
|
2117
|
+
score
|
|
2118
|
+
};
|
|
2119
|
+
}).filter((item) => item !== null);
|
|
2120
|
+
return parsed.sort((a, b) => {
|
|
2121
|
+
const indexA = preferredOrder.indexOf(a.key);
|
|
2122
|
+
const indexB = preferredOrder.indexOf(b.key);
|
|
2123
|
+
const rankA = indexA === -1 ? Number.MAX_SAFE_INTEGER : indexA;
|
|
2124
|
+
const rankB = indexB === -1 ? Number.MAX_SAFE_INTEGER : indexB;
|
|
2125
|
+
if (rankA !== rankB) {
|
|
2126
|
+
return rankA - rankB;
|
|
2127
|
+
}
|
|
2128
|
+
return a.label.localeCompare(b.label);
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
1945
2131
|
async function runSearch(args) {
|
|
1946
2132
|
const query = args.join(" ");
|
|
1947
2133
|
console.log();
|
|
@@ -1962,9 +2148,11 @@ async function runSearch(args) {
|
|
|
1962
2148
|
const displayName = skill.name || "unknown";
|
|
1963
2149
|
const owner = skill.owner || "unknown";
|
|
1964
2150
|
const description = skill.description || "";
|
|
2151
|
+
const aiScore = getTotalAIScore(skill);
|
|
1965
2152
|
console.log(` ${pc.cyan(displayName)} ${pc.dim(`by ${owner}`)}`);
|
|
2153
|
+
console.log(` ${pc.dim("AI score:")} ${formatScore(aiScore)}`);
|
|
1966
2154
|
if (description) {
|
|
1967
|
-
console.log(` ${pc.dim(description.slice(0,
|
|
2155
|
+
console.log(` ${pc.dim(description.slice(0, SEARCH_DESCRIPTION_MAX_LENGTH))}${description.length > SEARCH_DESCRIPTION_MAX_LENGTH ? "..." : ""}`);
|
|
1968
2156
|
}
|
|
1969
2157
|
const installCmd = skill.owner && skill.repo ? `gh:${skill.owner}/${skill.repo}@${displayName}` : `gh:${displayName}`;
|
|
1970
2158
|
console.log(` ${pc.dim("askill add")} ${installCmd}`);
|
|
@@ -2077,6 +2265,15 @@ async function runInfo(args) {
|
|
|
2077
2265
|
if (skill.stars !== null && skill.stars !== void 0) {
|
|
2078
2266
|
console.log(` ${pc.dim("Stars:")} ${skill.stars.toLocaleString()}`);
|
|
2079
2267
|
}
|
|
2268
|
+
const aiScore = getTotalAIScore(skill);
|
|
2269
|
+
console.log(` ${pc.dim("AI score:")} ${formatScore(aiScore)}`);
|
|
2270
|
+
const aiDimensions = getAIScoreDimensions(skill);
|
|
2271
|
+
if (aiDimensions.length > 0) {
|
|
2272
|
+
console.log(` ${pc.dim("AI breakdown:")}`);
|
|
2273
|
+
for (const dimension of aiDimensions) {
|
|
2274
|
+
console.log(` ${pc.dim(`${dimension.label}:`)} ${formatScore(dimension.score)}`);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2080
2277
|
if (skill.tags && skill.tags.length > 0) {
|
|
2081
2278
|
console.log(` ${pc.dim("Tags:")} ${skill.tags.join(", ")}`);
|
|
2082
2279
|
}
|
|
@@ -2312,33 +2509,33 @@ async function findSkillDir(skillName) {
|
|
|
2312
2509
|
const { access: fsAccess } = await import("fs/promises");
|
|
2313
2510
|
const sanitized = sanitizeName(skillName);
|
|
2314
2511
|
const cwd = process.cwd();
|
|
2315
|
-
const projectCanonical =
|
|
2512
|
+
const projectCanonical = join9(cwd, AGENTS_DIR, SKILLS_SUBDIR, sanitized);
|
|
2316
2513
|
try {
|
|
2317
|
-
await fsAccess(
|
|
2514
|
+
await fsAccess(join9(projectCanonical, "SKILL.md"));
|
|
2318
2515
|
return projectCanonical;
|
|
2319
2516
|
} catch {
|
|
2320
2517
|
}
|
|
2321
2518
|
const commonAgentDirs = [".claude/skills", ".cursor/skills", ".opencode/skills", ".windsurf/skills"];
|
|
2322
2519
|
for (const dir of commonAgentDirs) {
|
|
2323
|
-
const agentPath =
|
|
2520
|
+
const agentPath = join9(cwd, dir, sanitized);
|
|
2324
2521
|
try {
|
|
2325
|
-
await fsAccess(
|
|
2522
|
+
await fsAccess(join9(agentPath, "SKILL.md"));
|
|
2326
2523
|
return agentPath;
|
|
2327
2524
|
} catch {
|
|
2328
2525
|
}
|
|
2329
2526
|
}
|
|
2330
|
-
const home2 =
|
|
2331
|
-
const globalCanonical =
|
|
2527
|
+
const home2 = homedir7();
|
|
2528
|
+
const globalCanonical = join9(home2, AGENTS_DIR, SKILLS_SUBDIR, sanitized);
|
|
2332
2529
|
try {
|
|
2333
|
-
await fsAccess(
|
|
2530
|
+
await fsAccess(join9(globalCanonical, "SKILL.md"));
|
|
2334
2531
|
return globalCanonical;
|
|
2335
2532
|
} catch {
|
|
2336
2533
|
}
|
|
2337
2534
|
const globalAgentDirs = [".claude/skills", ".cursor/skills", ".opencode/skills"];
|
|
2338
2535
|
for (const dir of globalAgentDirs) {
|
|
2339
|
-
const agentPath =
|
|
2536
|
+
const agentPath = join9(home2, dir, sanitized);
|
|
2340
2537
|
try {
|
|
2341
|
-
await fsAccess(
|
|
2538
|
+
await fsAccess(join9(agentPath, "SKILL.md"));
|
|
2342
2539
|
return agentPath;
|
|
2343
2540
|
} catch {
|
|
2344
2541
|
}
|
|
@@ -2376,7 +2573,7 @@ Examples:`);
|
|
|
2376
2573
|
process.exit(1);
|
|
2377
2574
|
}
|
|
2378
2575
|
const fs = await import("fs/promises");
|
|
2379
|
-
const skillMdPath =
|
|
2576
|
+
const skillMdPath = join9(skillDir, "SKILL.md");
|
|
2380
2577
|
const content = await fs.readFile(skillMdPath, "utf-8");
|
|
2381
2578
|
const { frontmatter } = parseSkillMd(content);
|
|
2382
2579
|
if (!frontmatter.commands || Object.keys(frontmatter.commands).length === 0) {
|
|
@@ -2520,9 +2717,9 @@ function validateFrontmatter(frontmatter) {
|
|
|
2520
2717
|
async function runValidate(args) {
|
|
2521
2718
|
let targetPath = args.find((a) => !a.startsWith("-")) || "SKILL.md";
|
|
2522
2719
|
if (!targetPath.endsWith("SKILL.md")) {
|
|
2523
|
-
targetPath =
|
|
2720
|
+
targetPath = join9(targetPath, "SKILL.md");
|
|
2524
2721
|
}
|
|
2525
|
-
const absolutePath =
|
|
2722
|
+
const absolutePath = join9(process.cwd(), targetPath);
|
|
2526
2723
|
console.log();
|
|
2527
2724
|
p.intro(pc.bgCyan(pc.black(" askill validate ")));
|
|
2528
2725
|
const spinner2 = p.spinner();
|
|
@@ -2607,7 +2804,7 @@ async function runInit(args) {
|
|
|
2607
2804
|
const isYes = args.includes("-y") || args.includes("--yes");
|
|
2608
2805
|
console.log();
|
|
2609
2806
|
p.intro(pc.bgCyan(pc.black(" askill init ")));
|
|
2610
|
-
const skillPath =
|
|
2807
|
+
const skillPath = join9(process.cwd(), targetDir, "SKILL.md");
|
|
2611
2808
|
try {
|
|
2612
2809
|
await import("fs").then((fs2) => fs2.promises.access(skillPath));
|
|
2613
2810
|
p.log.error(`SKILL.md already exists at ${pc.cyan(skillPath)}`);
|
|
@@ -2729,7 +2926,7 @@ async function runInit(args) {
|
|
|
2729
2926
|
`;
|
|
2730
2927
|
content += `Provide concrete examples of when and how to use this skill.
|
|
2731
2928
|
`;
|
|
2732
|
-
const targetPath =
|
|
2929
|
+
const targetPath = join9(process.cwd(), targetDir);
|
|
2733
2930
|
if (targetDir !== ".") {
|
|
2734
2931
|
const fs2 = await import("fs");
|
|
2735
2932
|
await fs2.promises.mkdir(targetPath, { recursive: true });
|
|
@@ -2745,6 +2942,175 @@ async function runInit(args) {
|
|
|
2745
2942
|
console.log();
|
|
2746
2943
|
p.outro(pc.green("Done!"));
|
|
2747
2944
|
}
|
|
2945
|
+
async function runSubmit(args) {
|
|
2946
|
+
const url = args.find((a) => !a.startsWith("-"));
|
|
2947
|
+
if (!url) {
|
|
2948
|
+
console.log(`${RED}Usage: askill submit <github-url>${RESET}`);
|
|
2949
|
+
process.exit(1);
|
|
2950
|
+
}
|
|
2951
|
+
console.log();
|
|
2952
|
+
p.intro(pc.bgCyan(pc.black(" askill submit ")));
|
|
2953
|
+
const spinner2 = p.spinner();
|
|
2954
|
+
spinner2.start("Submitting URL for indexing...");
|
|
2955
|
+
try {
|
|
2956
|
+
const result = await api.submit(url);
|
|
2957
|
+
spinner2.stop(pc.green(result.message));
|
|
2958
|
+
console.log();
|
|
2959
|
+
for (const skill of result.skills) {
|
|
2960
|
+
const statusColor = skill.status === "indexed" ? pc.green : skill.status === "skipped" ? pc.yellow : pc.red;
|
|
2961
|
+
console.log(` ${statusColor(skill.status.padEnd(7))} ${pc.dim(skill.path)}${skill.name ? ` (${skill.name})` : ""}`);
|
|
2962
|
+
}
|
|
2963
|
+
p.outro(pc.green(`Submitted ${pc.cyan(`${result.repoOwner}/${result.repoName}`)}`));
|
|
2964
|
+
} catch (error) {
|
|
2965
|
+
spinner2.stop(pc.red("Submit failed"));
|
|
2966
|
+
if (error instanceof APIError) {
|
|
2967
|
+
p.log.error(error.message);
|
|
2968
|
+
} else {
|
|
2969
|
+
p.log.error(error instanceof Error ? error.message : "Unknown error");
|
|
2970
|
+
}
|
|
2971
|
+
p.outro(pc.red("Failed"));
|
|
2972
|
+
process.exit(1);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
async function runLogin(args) {
|
|
2976
|
+
let token = "";
|
|
2977
|
+
const tokenFlagIndex = args.findIndex((a) => a === "--token");
|
|
2978
|
+
if (tokenFlagIndex >= 0 && args[tokenFlagIndex + 1]) {
|
|
2979
|
+
token = args[tokenFlagIndex + 1];
|
|
2980
|
+
}
|
|
2981
|
+
if (!token) {
|
|
2982
|
+
console.log();
|
|
2983
|
+
p.note(`To get your token, visit: ${pc.cyan(`${REGISTRY_URL}/account`)}`);
|
|
2984
|
+
const input = await p.password({
|
|
2985
|
+
message: "API token",
|
|
2986
|
+
placeholder: "ask_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
2987
|
+
mask: "*",
|
|
2988
|
+
validate: (value) => {
|
|
2989
|
+
if (!value) return "Token is required";
|
|
2990
|
+
if (!value.startsWith("ask_")) return "Token must start with ask_";
|
|
2991
|
+
return void 0;
|
|
2992
|
+
}
|
|
2993
|
+
});
|
|
2994
|
+
if (p.isCancel(input)) {
|
|
2995
|
+
p.cancel("Login cancelled");
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
token = input;
|
|
2999
|
+
}
|
|
3000
|
+
const spinner2 = p.spinner();
|
|
3001
|
+
spinner2.start("Verifying token...");
|
|
3002
|
+
try {
|
|
3003
|
+
const me = await api.authMe(token);
|
|
3004
|
+
const username = me.username ?? void 0;
|
|
3005
|
+
await saveCredentials({ token, username });
|
|
3006
|
+
spinner2.stop(pc.green(`Logged in as @${username ?? "unknown"}`));
|
|
3007
|
+
p.outro(pc.green("Authentication saved"));
|
|
3008
|
+
} catch (error) {
|
|
3009
|
+
spinner2.stop(pc.red("Invalid token"));
|
|
3010
|
+
if (error instanceof APIError) {
|
|
3011
|
+
p.log.error(error.message);
|
|
3012
|
+
} else {
|
|
3013
|
+
p.log.error(error instanceof Error ? error.message : "Unknown error");
|
|
3014
|
+
}
|
|
3015
|
+
p.outro(pc.red("Login failed"));
|
|
3016
|
+
process.exit(1);
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
async function runLogout() {
|
|
3020
|
+
await clearCredentials();
|
|
3021
|
+
p.outro(pc.green("Logged out"));
|
|
3022
|
+
}
|
|
3023
|
+
async function runWhoami() {
|
|
3024
|
+
const creds = await loadCredentials();
|
|
3025
|
+
if (!creds) {
|
|
3026
|
+
p.log.warn("Not logged in. Run askill login first.");
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
3029
|
+
try {
|
|
3030
|
+
const me = await api.authMe(creds.token);
|
|
3031
|
+
const username = me.username ?? creds.username ?? "unknown";
|
|
3032
|
+
console.log(`@${username} (token: ${maskToken(creds.token)})`);
|
|
3033
|
+
} catch {
|
|
3034
|
+
console.log(`Stored token appears invalid (token: ${maskToken(creds.token)})`);
|
|
3035
|
+
process.exit(1);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
async function runPublish(args) {
|
|
3039
|
+
const creds = await loadCredentials();
|
|
3040
|
+
if (!creds?.token) {
|
|
3041
|
+
p.log.error("Not logged in. Run askill login first.");
|
|
3042
|
+
process.exit(1);
|
|
3043
|
+
}
|
|
3044
|
+
const githubFlagIndex = args.findIndex((a) => a === "--github");
|
|
3045
|
+
const githubUrl = githubFlagIndex >= 0 ? args[githubFlagIndex + 1] : void 0;
|
|
3046
|
+
const localPath = args.find((a) => !a.startsWith("-")) || ".";
|
|
3047
|
+
let content = "";
|
|
3048
|
+
if (githubUrl) {
|
|
3049
|
+
const rawUrl = toRawGitHubUrl(githubUrl);
|
|
3050
|
+
if (!rawUrl) {
|
|
3051
|
+
p.log.error("Invalid GitHub file URL. Use a blob URL to SKILL.md");
|
|
3052
|
+
process.exit(1);
|
|
3053
|
+
}
|
|
3054
|
+
const spinner3 = p.spinner();
|
|
3055
|
+
spinner3.start("Fetching SKILL.md from GitHub...");
|
|
3056
|
+
const res = await fetch(rawUrl);
|
|
3057
|
+
if (!res.ok) {
|
|
3058
|
+
spinner3.stop(pc.red("Fetch failed"));
|
|
3059
|
+
p.log.error("Unable to fetch SKILL.md from GitHub URL");
|
|
3060
|
+
process.exit(1);
|
|
3061
|
+
}
|
|
3062
|
+
content = await res.text();
|
|
3063
|
+
spinner3.stop("Fetched");
|
|
3064
|
+
} else {
|
|
3065
|
+
const fs = await import("fs/promises");
|
|
3066
|
+
const skillPath = localPath.endsWith("SKILL.md") ? localPath : join9(localPath, "SKILL.md");
|
|
3067
|
+
try {
|
|
3068
|
+
content = await fs.readFile(join9(process.cwd(), skillPath), "utf-8");
|
|
3069
|
+
} catch {
|
|
3070
|
+
p.log.error(`Cannot read ${pc.cyan(skillPath)}`);
|
|
3071
|
+
process.exit(1);
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
const parsed = parseSkillMd(content);
|
|
3075
|
+
const name = typeof parsed.frontmatter.name === "string" ? parsed.frontmatter.name.trim() : "";
|
|
3076
|
+
const version = typeof parsed.frontmatter.version === "string" ? parsed.frontmatter.version.trim() : "";
|
|
3077
|
+
if (!name) {
|
|
3078
|
+
p.log.error("SKILL.md must include frontmatter name");
|
|
3079
|
+
process.exit(1);
|
|
3080
|
+
}
|
|
3081
|
+
if (!version || !/^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/.test(version)) {
|
|
3082
|
+
p.log.error("SKILL.md must include a valid semver version");
|
|
3083
|
+
process.exit(1);
|
|
3084
|
+
}
|
|
3085
|
+
console.log();
|
|
3086
|
+
p.intro(pc.bgCyan(pc.black(" askill publish ")));
|
|
3087
|
+
const spinner2 = p.spinner();
|
|
3088
|
+
spinner2.start("Publishing skill...");
|
|
3089
|
+
try {
|
|
3090
|
+
const result = await api.publish({
|
|
3091
|
+
token: creds.token,
|
|
3092
|
+
githubUrl,
|
|
3093
|
+
content: githubUrl ? void 0 : content
|
|
3094
|
+
});
|
|
3095
|
+
spinner2.stop(pc.green(`Published ${result.slug}@${result.version}`));
|
|
3096
|
+
p.outro(pc.cyan(result.url));
|
|
3097
|
+
} catch (error) {
|
|
3098
|
+
spinner2.stop(pc.red("Publish failed"));
|
|
3099
|
+
if (error instanceof APIError) {
|
|
3100
|
+
p.log.error(error.message);
|
|
3101
|
+
} else {
|
|
3102
|
+
p.log.error(error instanceof Error ? error.message : "Unknown error");
|
|
3103
|
+
}
|
|
3104
|
+
p.outro(pc.red("Failed"));
|
|
3105
|
+
process.exit(1);
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
function toRawGitHubUrl(url) {
|
|
3109
|
+
const match = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/[^/]+\/(.+)$/);
|
|
3110
|
+
if (!match) return null;
|
|
3111
|
+
const [, owner, repo, path] = match;
|
|
3112
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/HEAD/${path}`;
|
|
3113
|
+
}
|
|
2748
3114
|
async function main() {
|
|
2749
3115
|
const args = process.argv.slice(2);
|
|
2750
3116
|
checkForUpdates().catch(() => {
|
|
@@ -2797,6 +3163,21 @@ async function main() {
|
|
|
2797
3163
|
case "init":
|
|
2798
3164
|
await runInit(restArgs);
|
|
2799
3165
|
break;
|
|
3166
|
+
case "submit":
|
|
3167
|
+
await runSubmit(restArgs);
|
|
3168
|
+
break;
|
|
3169
|
+
case "login":
|
|
3170
|
+
await runLogin(restArgs);
|
|
3171
|
+
break;
|
|
3172
|
+
case "logout":
|
|
3173
|
+
await runLogout();
|
|
3174
|
+
break;
|
|
3175
|
+
case "whoami":
|
|
3176
|
+
await runWhoami();
|
|
3177
|
+
break;
|
|
3178
|
+
case "publish":
|
|
3179
|
+
await runPublish(restArgs);
|
|
3180
|
+
break;
|
|
2800
3181
|
case "--help":
|
|
2801
3182
|
case "-h":
|
|
2802
3183
|
case "help":
|