askill-cli 0.1.1 → 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.
Files changed (3) hide show
  1. package/README.md +13 -3
  2. package/dist/cli.mjs +444 -59
  3. 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,11 +57,13 @@ 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
56
65
  # Install
57
- curl -fsSL https://askill.sh/install.sh | sh
66
+ curl -fsSL https://askill.sh | sh
58
67
 
59
68
  # Install a skill
60
69
  askill add owner/repo@skill-name
@@ -71,7 +80,7 @@ askill list
71
80
  ### One-line install (recommended)
72
81
 
73
82
  ```bash
74
- curl -fsSL https://askill.sh/install.sh | sh
83
+ curl -fsSL https://askill.sh | sh
75
84
  ```
76
85
 
77
86
  ### npm
@@ -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. Publish: Push to GitHub, it's automatically indexed
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,8 +4,9 @@
4
4
  import { homedir } from "os";
5
5
  import { join } from "path";
6
6
  import { existsSync } from "fs";
7
- var VERSION = "0.1.1";
7
+ var VERSION = "0.1.3";
8
8
  var API_BASE_URL = "https://askill.sh/api/v1";
9
+ var REGISTRY_URL = "https://askill.sh";
9
10
  var RESET = "\x1B[0m";
10
11
  var BOLD = "\x1B[1m";
11
12
  var DIM = "\x1B[2m";
@@ -395,6 +396,40 @@ var APIClient = class {
395
396
  async checkCLIVersion() {
396
397
  return this.fetch("/cli/version");
397
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
+ }
398
433
  };
399
434
  var APIError = class extends Error {
400
435
  constructor(status, code, message) {
@@ -694,8 +729,8 @@ function getPlatformKey() {
694
729
  }
695
730
  async function shouldCheckUpdate() {
696
731
  try {
697
- const { readFile: readFile4 } = await import("fs/promises");
698
- const lastCheck = await readFile4(UPDATE_CHECK_FILE, "utf-8");
732
+ const { readFile: readFile5 } = await import("fs/promises");
733
+ const lastCheck = await readFile5(UPDATE_CHECK_FILE, "utf-8");
699
734
  const lastCheckTime = parseInt(lastCheck, 10);
700
735
  return Date.now() - lastCheckTime > UPDATE_INTERVAL_MS;
701
736
  } catch {
@@ -704,9 +739,9 @@ async function shouldCheckUpdate() {
704
739
  }
705
740
  async function saveUpdateCheckTime() {
706
741
  try {
707
- const { mkdir: mkdir4, writeFile: writeFile4 } = await import("fs/promises");
708
- await mkdir4(dirname2(UPDATE_CHECK_FILE), { recursive: true });
709
- await writeFile4(UPDATE_CHECK_FILE, String(Date.now()), "utf-8");
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");
710
745
  } catch {
711
746
  }
712
747
  }
@@ -795,7 +830,7 @@ async function selfUpdate() {
795
830
  if (!downloadUrl) {
796
831
  console.log(`${RED}No download available for your platform (${platformKey})${RESET}`);
797
832
  console.log(`Please update manually:`);
798
- console.log(` ${CYAN}curl -fsSL https://askill.sh/install.sh | sh${RESET}`);
833
+ console.log(` ${CYAN}curl -fsSL https://askill.sh | sh${RESET}`);
799
834
  console.log(` ${DIM}or${RESET}`);
800
835
  console.log(` ${CYAN}npm install -g askill-cli@latest${RESET}`);
801
836
  return false;
@@ -837,7 +872,7 @@ async function selfUpdate() {
837
872
  } catch (error) {
838
873
  console.log(`${RED}Update failed: ${error instanceof Error ? error.message : "Unknown error"}${RESET}`);
839
874
  console.log(`Please update manually:`);
840
- console.log(` ${CYAN}curl -fsSL https://askill.sh/install.sh | sh${RESET}`);
875
+ console.log(` ${CYAN}curl -fsSL https://askill.sh | sh${RESET}`);
841
876
  console.log(` ${DIM}or${RESET}`);
842
877
  console.log(` ${CYAN}npm install -g askill-cli@latest${RESET}`);
843
878
  return false;
@@ -864,6 +899,37 @@ async function getPreferredAgents() {
864
899
  return config.preferredAgents;
865
900
  }
866
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
+
867
933
  // src/parser.ts
868
934
  function parseSkillMd(content) {
869
935
  const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
@@ -1126,12 +1192,12 @@ function isLocalPath(input) {
1126
1192
  }
1127
1193
 
1128
1194
  // src/discover.ts
1129
- import { readdir as readdir2, readFile as readFile2, stat } from "fs/promises";
1130
- import { join as join5 } from "path";
1195
+ import { readdir as readdir2, readFile as readFile3, stat } from "fs/promises";
1196
+ import { join as join6 } from "path";
1131
1197
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "__pycache__"]);
1132
1198
  async function hasSkillMd(dir) {
1133
1199
  try {
1134
- const skillPath = join5(dir, "SKILL.md");
1200
+ const skillPath = join6(dir, "SKILL.md");
1135
1201
  const stats = await stat(skillPath);
1136
1202
  return stats.isFile();
1137
1203
  } catch {
@@ -1140,8 +1206,8 @@ async function hasSkillMd(dir) {
1140
1206
  }
1141
1207
  async function parseSkillDir(dir) {
1142
1208
  try {
1143
- const skillPath = join5(dir, "SKILL.md");
1144
- const content = await readFile2(skillPath, "utf-8");
1209
+ const skillPath = join6(dir, "SKILL.md");
1210
+ const content = await readFile3(skillPath, "utf-8");
1145
1211
  const parsed = parseSkillMd(content);
1146
1212
  if (!parsed.frontmatter.name || !parsed.frontmatter.description) {
1147
1213
  return null;
@@ -1166,7 +1232,7 @@ async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
1166
1232
  ]);
1167
1233
  const currentDir = hasSkill ? [dir] : [];
1168
1234
  const subDirResults = await Promise.all(
1169
- entries.filter((entry) => entry.isDirectory() && !SKIP_DIRS.has(entry.name)).map((entry) => findSkillDirs(join5(dir, entry.name), depth + 1, maxDepth))
1235
+ entries.filter((entry) => entry.isDirectory() && !SKIP_DIRS.has(entry.name)).map((entry) => findSkillDirs(join6(dir, entry.name), depth + 1, maxDepth))
1170
1236
  );
1171
1237
  return [...currentDir, ...subDirResults.flat()];
1172
1238
  } catch {
@@ -1176,7 +1242,7 @@ async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
1176
1242
  async function discoverSkills(basePath, subpath) {
1177
1243
  const skills = [];
1178
1244
  const seenNames = /* @__PURE__ */ new Set();
1179
- const searchPath = subpath ? join5(basePath, subpath) : basePath;
1245
+ const searchPath = subpath ? join6(basePath, subpath) : basePath;
1180
1246
  if (await hasSkillMd(searchPath)) {
1181
1247
  const skill = await parseSkillDir(searchPath);
1182
1248
  if (skill) {
@@ -1185,27 +1251,27 @@ async function discoverSkills(basePath, subpath) {
1185
1251
  }
1186
1252
  const prioritySearchDirs = [
1187
1253
  searchPath,
1188
- join5(searchPath, "skills"),
1189
- join5(searchPath, "skills/.curated"),
1190
- join5(searchPath, "skills/.experimental"),
1191
- join5(searchPath, ".agents/skills"),
1192
- join5(searchPath, ".claude/skills"),
1193
- join5(searchPath, ".opencode/skills"),
1194
- join5(searchPath, ".cursor/skills"),
1195
- join5(searchPath, ".codex/skills"),
1196
- join5(searchPath, ".cline/skills"),
1197
- join5(searchPath, ".gemini/skills"),
1198
- join5(searchPath, ".windsurf/skills"),
1199
- join5(searchPath, ".roo/skills"),
1200
- join5(searchPath, ".github/skills"),
1201
- join5(searchPath, ".goose/skills")
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")
1202
1268
  ];
1203
1269
  for (const dir of prioritySearchDirs) {
1204
1270
  try {
1205
1271
  const entries = await readdir2(dir, { withFileTypes: true });
1206
1272
  for (const entry of entries) {
1207
1273
  if (entry.isDirectory()) {
1208
- const skillDir = join5(dir, entry.name);
1274
+ const skillDir = join6(dir, entry.name);
1209
1275
  if (await hasSkillMd(skillDir)) {
1210
1276
  const skill = await parseSkillDir(skillDir);
1211
1277
  if (skill && !seenNames.has(skill.name)) {
@@ -1240,8 +1306,8 @@ function filterSkills(skills, names) {
1240
1306
 
1241
1307
  // src/git.ts
1242
1308
  import { execFile } from "child_process";
1243
- import { mkdtemp, rm as rm2 } from "fs/promises";
1244
- import { join as join6, normalize as normalize2, resolve as resolve3, sep as sep2 } from "path";
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";
1245
1311
  import { tmpdir } from "os";
1246
1312
  var CLONE_TIMEOUT_MS = 6e4;
1247
1313
  var GitCloneError = class extends Error {
@@ -1257,7 +1323,7 @@ var GitCloneError = class extends Error {
1257
1323
  }
1258
1324
  };
1259
1325
  async function cloneRepo(url, ref) {
1260
- const tempDir = await mkdtemp(join6(tmpdir(), "askill-"));
1326
+ const tempDir = await mkdtemp(join7(tmpdir(), "askill-"));
1261
1327
  const args = ["clone", "--depth", "1"];
1262
1328
  if (ref) {
1263
1329
  args.push("--branch", ref);
@@ -1267,7 +1333,7 @@ async function cloneRepo(url, ref) {
1267
1333
  await execGit(args);
1268
1334
  return tempDir;
1269
1335
  } catch (error) {
1270
- await rm2(tempDir, { recursive: true, force: true }).catch(() => {
1336
+ await rm3(tempDir, { recursive: true, force: true }).catch(() => {
1271
1337
  });
1272
1338
  const errorMessage = error instanceof Error ? error.message : String(error);
1273
1339
  const isTimeout = errorMessage.includes("timed out") || errorMessage.includes("timeout");
@@ -1300,7 +1366,7 @@ async function cleanupTempDir(dir) {
1300
1366
  if (!normalizedDir.startsWith(normalizedTmpDir + sep2) && normalizedDir !== normalizedTmpDir) {
1301
1367
  throw new Error("Attempted to clean up directory outside of temp directory");
1302
1368
  }
1303
- await rm2(dir, { recursive: true, force: true });
1369
+ await rm3(dir, { recursive: true, force: true });
1304
1370
  }
1305
1371
  function execGit(args) {
1306
1372
  return new Promise((resolve4, reject) => {
@@ -1315,14 +1381,14 @@ function execGit(args) {
1315
1381
  }
1316
1382
 
1317
1383
  // src/lock.ts
1318
- import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
1319
- import { join as join7, dirname as dirname5 } from "path";
1320
- import { homedir as homedir5 } from "os";
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";
1321
1387
  var AGENTS_DIR2 = ".agents";
1322
1388
  var LOCK_FILE = ".skill-lock.json";
1323
1389
  var CURRENT_VERSION = 3;
1324
1390
  function getSkillLockPath() {
1325
- return join7(homedir5(), AGENTS_DIR2, LOCK_FILE);
1391
+ return join8(homedir6(), AGENTS_DIR2, LOCK_FILE);
1326
1392
  }
1327
1393
  function createEmptyLockFile() {
1328
1394
  return {
@@ -1333,7 +1399,7 @@ function createEmptyLockFile() {
1333
1399
  async function readSkillLock() {
1334
1400
  const lockPath = getSkillLockPath();
1335
1401
  try {
1336
- const content = await readFile3(lockPath, "utf-8");
1402
+ const content = await readFile4(lockPath, "utf-8");
1337
1403
  const parsed = JSON.parse(content);
1338
1404
  if (typeof parsed.version !== "number" || !parsed.skills) {
1339
1405
  return createEmptyLockFile();
@@ -1348,9 +1414,9 @@ async function readSkillLock() {
1348
1414
  }
1349
1415
  async function writeSkillLock(lock) {
1350
1416
  const lockPath = getSkillLockPath();
1351
- await mkdir3(dirname5(lockPath), { recursive: true });
1417
+ await mkdir4(dirname5(lockPath), { recursive: true });
1352
1418
  const content = JSON.stringify(lock, null, 2);
1353
- await writeFile3(lockPath, content, "utf-8");
1419
+ await writeFile4(lockPath, content, "utf-8");
1354
1420
  }
1355
1421
  async function addSkillToLock(skillName, entry) {
1356
1422
  const lock = await readSkillLock();
@@ -1424,8 +1490,8 @@ async function fetchSkillFolderHash(ownerRepo, skillPath) {
1424
1490
  }
1425
1491
 
1426
1492
  // src/cli.ts
1427
- import { join as join8 } from "path";
1428
- import { homedir as homedir6 } from "os";
1493
+ import { join as join9 } from "path";
1494
+ import { homedir as homedir7 } from "os";
1429
1495
  import * as p from "@clack/prompts";
1430
1496
  import pc from "picocolors";
1431
1497
  var LOGO = `
@@ -1454,6 +1520,9 @@ function showBanner() {
1454
1520
  console.log(` ${DIM}$${RESET} askill list${RESET} ${DIM}List installed skills${RESET}`);
1455
1521
  console.log(` ${DIM}$${RESET} askill remove ${DIM}<skill>${RESET} ${DIM}Remove a skill${RESET}`);
1456
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}`);
1457
1526
  console.log(` ${DIM}$${RESET} askill run ${DIM}<skill:cmd>${RESET} ${DIM}Run a skill command${RESET}`);
1458
1527
  console.log();
1459
1528
  console.log(`${DIM}Browse skills at${RESET} ${CYAN}https://askill.sh${RESET}`);
@@ -1473,6 +1542,12 @@ ${BOLD}Commands:${RESET}
1473
1542
  validate [path] Validate a SKILL.md file
1474
1543
  check Check installed skills for updates
1475
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
1476
1551
  run <skill:cmd> Run a skill command
1477
1552
  upgrade Update askill CLI to latest version
1478
1553
 
@@ -1941,6 +2016,118 @@ Formats supported:`);
1941
2016
  await cleanup();
1942
2017
  }
1943
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
+ }
1944
2131
  async function runSearch(args) {
1945
2132
  const query = args.join(" ");
1946
2133
  console.log();
@@ -1961,12 +2148,17 @@ async function runSearch(args) {
1961
2148
  const displayName = skill.name || "unknown";
1962
2149
  const owner = skill.owner || "unknown";
1963
2150
  const description = skill.description || "";
2151
+ const aiScore = getTotalAIScore(skill);
1964
2152
  console.log(` ${pc.cyan(displayName)} ${pc.dim(`by ${owner}`)}`);
2153
+ console.log(` ${pc.dim("AI score:")} ${formatScore(aiScore)}`);
1965
2154
  if (description) {
1966
- console.log(` ${pc.dim(description.slice(0, 80))}${description.length > 80 ? "..." : ""}`);
2155
+ console.log(` ${pc.dim(description.slice(0, SEARCH_DESCRIPTION_MAX_LENGTH))}${description.length > SEARCH_DESCRIPTION_MAX_LENGTH ? "..." : ""}`);
1967
2156
  }
1968
2157
  const installCmd = skill.owner && skill.repo ? `gh:${skill.owner}/${skill.repo}@${displayName}` : `gh:${displayName}`;
1969
2158
  console.log(` ${pc.dim("askill add")} ${installCmd}`);
2159
+ if (skill.id) {
2160
+ console.log(` ${pc.dim(REGISTRY_URL + "/skills/" + skill.id)}`);
2161
+ }
1970
2162
  console.log();
1971
2163
  }
1972
2164
  p.outro(`Browse more at ${pc.cyan("https://askill.sh")}`);
@@ -2073,6 +2265,15 @@ async function runInfo(args) {
2073
2265
  if (skill.stars !== null && skill.stars !== void 0) {
2074
2266
  console.log(` ${pc.dim("Stars:")} ${skill.stars.toLocaleString()}`);
2075
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
+ }
2076
2277
  if (skill.tags && skill.tags.length > 0) {
2077
2278
  console.log(` ${pc.dim("Tags:")} ${skill.tags.join(", ")}`);
2078
2279
  }
@@ -2308,33 +2509,33 @@ async function findSkillDir(skillName) {
2308
2509
  const { access: fsAccess } = await import("fs/promises");
2309
2510
  const sanitized = sanitizeName(skillName);
2310
2511
  const cwd = process.cwd();
2311
- const projectCanonical = join8(cwd, AGENTS_DIR, SKILLS_SUBDIR, sanitized);
2512
+ const projectCanonical = join9(cwd, AGENTS_DIR, SKILLS_SUBDIR, sanitized);
2312
2513
  try {
2313
- await fsAccess(join8(projectCanonical, "SKILL.md"));
2514
+ await fsAccess(join9(projectCanonical, "SKILL.md"));
2314
2515
  return projectCanonical;
2315
2516
  } catch {
2316
2517
  }
2317
2518
  const commonAgentDirs = [".claude/skills", ".cursor/skills", ".opencode/skills", ".windsurf/skills"];
2318
2519
  for (const dir of commonAgentDirs) {
2319
- const agentPath = join8(cwd, dir, sanitized);
2520
+ const agentPath = join9(cwd, dir, sanitized);
2320
2521
  try {
2321
- await fsAccess(join8(agentPath, "SKILL.md"));
2522
+ await fsAccess(join9(agentPath, "SKILL.md"));
2322
2523
  return agentPath;
2323
2524
  } catch {
2324
2525
  }
2325
2526
  }
2326
- const home2 = homedir6();
2327
- const globalCanonical = join8(home2, AGENTS_DIR, SKILLS_SUBDIR, sanitized);
2527
+ const home2 = homedir7();
2528
+ const globalCanonical = join9(home2, AGENTS_DIR, SKILLS_SUBDIR, sanitized);
2328
2529
  try {
2329
- await fsAccess(join8(globalCanonical, "SKILL.md"));
2530
+ await fsAccess(join9(globalCanonical, "SKILL.md"));
2330
2531
  return globalCanonical;
2331
2532
  } catch {
2332
2533
  }
2333
2534
  const globalAgentDirs = [".claude/skills", ".cursor/skills", ".opencode/skills"];
2334
2535
  for (const dir of globalAgentDirs) {
2335
- const agentPath = join8(home2, dir, sanitized);
2536
+ const agentPath = join9(home2, dir, sanitized);
2336
2537
  try {
2337
- await fsAccess(join8(agentPath, "SKILL.md"));
2538
+ await fsAccess(join9(agentPath, "SKILL.md"));
2338
2539
  return agentPath;
2339
2540
  } catch {
2340
2541
  }
@@ -2372,7 +2573,7 @@ Examples:`);
2372
2573
  process.exit(1);
2373
2574
  }
2374
2575
  const fs = await import("fs/promises");
2375
- const skillMdPath = join8(skillDir, "SKILL.md");
2576
+ const skillMdPath = join9(skillDir, "SKILL.md");
2376
2577
  const content = await fs.readFile(skillMdPath, "utf-8");
2377
2578
  const { frontmatter } = parseSkillMd(content);
2378
2579
  if (!frontmatter.commands || Object.keys(frontmatter.commands).length === 0) {
@@ -2516,9 +2717,9 @@ function validateFrontmatter(frontmatter) {
2516
2717
  async function runValidate(args) {
2517
2718
  let targetPath = args.find((a) => !a.startsWith("-")) || "SKILL.md";
2518
2719
  if (!targetPath.endsWith("SKILL.md")) {
2519
- targetPath = join8(targetPath, "SKILL.md");
2720
+ targetPath = join9(targetPath, "SKILL.md");
2520
2721
  }
2521
- const absolutePath = join8(process.cwd(), targetPath);
2722
+ const absolutePath = join9(process.cwd(), targetPath);
2522
2723
  console.log();
2523
2724
  p.intro(pc.bgCyan(pc.black(" askill validate ")));
2524
2725
  const spinner2 = p.spinner();
@@ -2603,7 +2804,7 @@ async function runInit(args) {
2603
2804
  const isYes = args.includes("-y") || args.includes("--yes");
2604
2805
  console.log();
2605
2806
  p.intro(pc.bgCyan(pc.black(" askill init ")));
2606
- const skillPath = join8(process.cwd(), targetDir, "SKILL.md");
2807
+ const skillPath = join9(process.cwd(), targetDir, "SKILL.md");
2607
2808
  try {
2608
2809
  await import("fs").then((fs2) => fs2.promises.access(skillPath));
2609
2810
  p.log.error(`SKILL.md already exists at ${pc.cyan(skillPath)}`);
@@ -2725,7 +2926,7 @@ async function runInit(args) {
2725
2926
  `;
2726
2927
  content += `Provide concrete examples of when and how to use this skill.
2727
2928
  `;
2728
- const targetPath = join8(process.cwd(), targetDir);
2929
+ const targetPath = join9(process.cwd(), targetDir);
2729
2930
  if (targetDir !== ".") {
2730
2931
  const fs2 = await import("fs");
2731
2932
  await fs2.promises.mkdir(targetPath, { recursive: true });
@@ -2741,6 +2942,175 @@ async function runInit(args) {
2741
2942
  console.log();
2742
2943
  p.outro(pc.green("Done!"));
2743
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
+ }
2744
3114
  async function main() {
2745
3115
  const args = process.argv.slice(2);
2746
3116
  checkForUpdates().catch(() => {
@@ -2793,6 +3163,21 @@ async function main() {
2793
3163
  case "init":
2794
3164
  await runInit(restArgs);
2795
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;
2796
3181
  case "--help":
2797
3182
  case "-h":
2798
3183
  case "help":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "askill-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "askill - The Agent Skill Package Manager",
5
5
  "type": "module",
6
6
  "bin": {