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.
Files changed (3) hide show
  1. package/README.md +11 -1
  2. package/dist/cli.mjs +438 -57
  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,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. 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,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.2";
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: readFile4 } = await import("fs/promises");
699
- 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");
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: mkdir4, writeFile: writeFile4 } = await import("fs/promises");
709
- await mkdir4(dirname2(UPDATE_CHECK_FILE), { recursive: true });
710
- 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");
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 readFile2, stat } from "fs/promises";
1131
- 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";
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 = join5(dir, "SKILL.md");
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 = join5(dir, "SKILL.md");
1145
- const content = await readFile2(skillPath, "utf-8");
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(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))
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 ? join5(basePath, subpath) : basePath;
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
- join5(searchPath, "skills"),
1190
- join5(searchPath, "skills/.curated"),
1191
- join5(searchPath, "skills/.experimental"),
1192
- join5(searchPath, ".agents/skills"),
1193
- join5(searchPath, ".claude/skills"),
1194
- join5(searchPath, ".opencode/skills"),
1195
- join5(searchPath, ".cursor/skills"),
1196
- join5(searchPath, ".codex/skills"),
1197
- join5(searchPath, ".cline/skills"),
1198
- join5(searchPath, ".gemini/skills"),
1199
- join5(searchPath, ".windsurf/skills"),
1200
- join5(searchPath, ".roo/skills"),
1201
- join5(searchPath, ".github/skills"),
1202
- 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")
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 = join5(dir, entry.name);
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 rm2 } from "fs/promises";
1245
- 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";
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(join6(tmpdir(), "askill-"));
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 rm2(tempDir, { recursive: true, force: true }).catch(() => {
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 rm2(dir, { recursive: true, force: true });
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 readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
1320
- import { join as join7, dirname as dirname5 } from "path";
1321
- 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";
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 join7(homedir5(), AGENTS_DIR2, LOCK_FILE);
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 readFile3(lockPath, "utf-8");
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 mkdir3(dirname5(lockPath), { recursive: true });
1417
+ await mkdir4(dirname5(lockPath), { recursive: true });
1353
1418
  const content = JSON.stringify(lock, null, 2);
1354
- await writeFile3(lockPath, content, "utf-8");
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 join8 } from "path";
1429
- import { homedir as homedir6 } from "os";
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, 80))}${description.length > 80 ? "..." : ""}`);
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 = join8(cwd, AGENTS_DIR, SKILLS_SUBDIR, sanitized);
2512
+ const projectCanonical = join9(cwd, AGENTS_DIR, SKILLS_SUBDIR, sanitized);
2316
2513
  try {
2317
- await fsAccess(join8(projectCanonical, "SKILL.md"));
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 = join8(cwd, dir, sanitized);
2520
+ const agentPath = join9(cwd, dir, sanitized);
2324
2521
  try {
2325
- await fsAccess(join8(agentPath, "SKILL.md"));
2522
+ await fsAccess(join9(agentPath, "SKILL.md"));
2326
2523
  return agentPath;
2327
2524
  } catch {
2328
2525
  }
2329
2526
  }
2330
- const home2 = homedir6();
2331
- const globalCanonical = join8(home2, AGENTS_DIR, SKILLS_SUBDIR, sanitized);
2527
+ const home2 = homedir7();
2528
+ const globalCanonical = join9(home2, AGENTS_DIR, SKILLS_SUBDIR, sanitized);
2332
2529
  try {
2333
- await fsAccess(join8(globalCanonical, "SKILL.md"));
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 = join8(home2, dir, sanitized);
2536
+ const agentPath = join9(home2, dir, sanitized);
2340
2537
  try {
2341
- await fsAccess(join8(agentPath, "SKILL.md"));
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 = join8(skillDir, "SKILL.md");
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 = join8(targetPath, "SKILL.md");
2720
+ targetPath = join9(targetPath, "SKILL.md");
2524
2721
  }
2525
- const absolutePath = join8(process.cwd(), targetPath);
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 = join8(process.cwd(), targetDir, "SKILL.md");
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 = join8(process.cwd(), targetDir);
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":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "askill-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "askill - The Agent Skill Package Manager",
5
5
  "type": "module",
6
6
  "bin": {