skillscat 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.
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import pc from 'picocolors';
4
- import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync, readdirSync, rmSync, realpathSync, statSync } from 'node:fs';
5
- import { join, dirname, posix, resolve, relative, isAbsolute } from 'node:path';
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync, readdirSync, rmSync, cpSync, realpathSync, statSync } from 'node:fs';
5
+ import { join, dirname, posix, basename, resolve, relative, isAbsolute } from 'node:path';
6
6
  import { createHash, randomBytes } from 'node:crypto';
7
7
  import os, { platform, homedir, hostname, release } from 'node:os';
8
8
  import * as readline from 'node:readline';
9
- import { spawnSync, execFileSync } from 'node:child_process';
9
+ import { spawnSync, execFileSync, execFile } from 'node:child_process';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { createServer } from 'node:http';
12
12
 
@@ -98,6 +98,7 @@ const SKILL_DISCOVERY_PATHS = [
98
98
  'skills/.experimental',
99
99
  'skills/.system',
100
100
  '.opencode/skill',
101
+ '.agents',
101
102
  '.claude/skills',
102
103
  '.codex/skills',
103
104
  '.cursor/skills',
@@ -1103,7 +1104,6 @@ async function fetchGitHubSkillCompanionFiles(source, skillFilePath, snapshot) {
1103
1104
  const tree = await snapshot.getTree();
1104
1105
  const normalizedSkillFilePath = normalizeRepoPath(skillFilePath);
1105
1106
  const skillDir = getRepoDirPath(normalizedSkillFilePath);
1106
- const nestedSkillDirs = getNestedSkillDirectories(tree, normalizedSkillFilePath);
1107
1107
  const pathMap = await snapshot.getPathMap();
1108
1108
  const files = [];
1109
1109
  for (const item of tree) {
@@ -1114,8 +1114,6 @@ async function fetchGitHubSkillCompanionFiles(source, skillFilePath, snapshot) {
1114
1114
  continue;
1115
1115
  if (!isPathWithinDirectory(repoPath, skillDir))
1116
1116
  continue;
1117
- if (isPathInNestedSkillDirectory(repoPath, nestedSkillDirs))
1118
- continue;
1119
1117
  const relativePath = toRelativeSkillPath(repoPath, skillDir);
1120
1118
  if (!relativePath)
1121
1119
  continue;
@@ -1168,32 +1166,6 @@ async function resolveGitHubBlobOrSymlinkContent({ snapshot, item, currentPath,
1168
1166
  visited: nextVisited,
1169
1167
  });
1170
1168
  }
1171
- function getNestedSkillDirectories(tree, currentSkillFilePath) {
1172
- const currentSkillDir = getRepoDirPath(currentSkillFilePath);
1173
- const nested = new Set();
1174
- for (const item of tree) {
1175
- const itemPath = normalizeRepoPath(item.path);
1176
- if (item.type !== 'blob' || !itemPath.endsWith('/SKILL.md'))
1177
- continue;
1178
- if (itemPath === currentSkillFilePath)
1179
- continue;
1180
- const dir = getRepoDirPath(itemPath);
1181
- if (dir === currentSkillDir)
1182
- continue;
1183
- if (!isPathWithinDirectory(dir, currentSkillDir))
1184
- continue;
1185
- nested.add(dir);
1186
- }
1187
- return Array.from(nested);
1188
- }
1189
- function isPathInNestedSkillDirectory(path, nestedSkillDirs) {
1190
- for (const nestedDir of nestedSkillDirs) {
1191
- if (isPathWithinDirectory(path, nestedDir)) {
1192
- return true;
1193
- }
1194
- }
1195
- return false;
1196
- }
1197
1169
  function normalizeRepoPath(path) {
1198
1170
  const normalized = path.replace(/^\/+|\/+$/g, '');
1199
1171
  if (!normalized)
@@ -1658,6 +1630,27 @@ function parseSlug(slug) {
1658
1630
  }
1659
1631
  return { owner: match[1], name: match[2] };
1660
1632
  }
1633
+ /**
1634
+ * Encode a slug for use in /skills/[owner]/[...name] paths
1635
+ * @param slug - Skill slug in format "owner/name[/nested]"
1636
+ * @returns Encoded path segment like "owner/name" (without leading slash)
1637
+ */
1638
+ function encodeSlugForSkillPath(slug) {
1639
+ const { owner, name } = parseSlug(slug);
1640
+ const encodedName = name
1641
+ .split('/')
1642
+ .map((segment) => encodeURIComponent(segment))
1643
+ .join('/');
1644
+ return `${encodeURIComponent(owner)}/${encodedName}`;
1645
+ }
1646
+ /**
1647
+ * Build the canonical web skill path from a slug
1648
+ * @param slug - Skill slug in format "owner/name[/nested]"
1649
+ * @returns Path like "/skills/owner/name"
1650
+ */
1651
+ function buildSkillPath(slug) {
1652
+ return `/skills/${encodeSlugForSkillPath(slug)}`;
1653
+ }
1661
1654
 
1662
1655
  const GITHUB_API = 'https://api.github.com';
1663
1656
  async function getAuthHeaders() {
@@ -1887,6 +1880,7 @@ function submitRepoForIndexingInBackground(source) {
1887
1880
  }
1888
1881
  }
1889
1882
 
1883
+ const FALLBACK_AGENT_ID = 'agents';
1890
1884
  function sanitizeSkillDirName(skillName) {
1891
1885
  const sanitized = skillName
1892
1886
  .replace(/[\\/]/g, '-')
@@ -1902,6 +1896,13 @@ function sanitizeSkillDirName(skillName) {
1902
1896
  return sanitized;
1903
1897
  }
1904
1898
  const AGENTS = [
1899
+ {
1900
+ id: FALLBACK_AGENT_ID,
1901
+ name: '.agents',
1902
+ projectPath: '.agents/',
1903
+ globalPath: join(homedir(), '.agents'),
1904
+ aliases: ['.agents', 'generic']
1905
+ },
1905
1906
  {
1906
1907
  id: 'amp',
1907
1908
  name: 'Amp',
@@ -1992,6 +1993,12 @@ const AGENTS = [
1992
1993
  projectPath: '.opencode/skill/',
1993
1994
  globalPath: join(homedir(), '.config', 'opencode', 'skill')
1994
1995
  },
1996
+ {
1997
+ id: 'openclaw',
1998
+ name: 'OpenClaw',
1999
+ projectPath: 'skills/',
2000
+ globalPath: join(homedir(), '.openclaw', 'skills')
2001
+ },
1995
2002
  {
1996
2003
  id: 'qoder',
1997
2004
  name: 'Qoder',
@@ -2017,21 +2024,51 @@ const AGENTS = [
2017
2024
  globalPath: join(homedir(), '.codeium', 'windsurf', 'skills')
2018
2025
  }
2019
2026
  ];
2027
+ function normalizeAgentId(id) {
2028
+ return id.trim().toLowerCase().replace(/\s+/g, '-');
2029
+ }
2030
+ function getGlobalDetectionPath(agent) {
2031
+ const normalized = agent.globalPath.replace(/[\\/]+$/, '');
2032
+ const container = normalized.replace(/[\\/](?:skill|skills)$/i, '');
2033
+ return container || normalized;
2034
+ }
2035
+ function getAgentBasePath(agent, global, cwd = process.cwd()) {
2036
+ return global ? agent.globalPath : join(cwd, agent.projectPath);
2037
+ }
2020
2038
  /**
2021
2039
  * Detect which agents are installed by checking for their config directories
2022
2040
  */
2023
2041
  function detectInstalledAgents() {
2024
2042
  return AGENTS.filter(agent => {
2025
- // Check if global path exists (indicating agent is installed)
2026
- const globalDir = agent.globalPath.replace(/\/skills\/?$/, '').replace(/\/skill\/?$/, '');
2027
- return existsSync(globalDir);
2043
+ const globalDir = getGlobalDetectionPath(agent);
2044
+ return existsSync(agent.globalPath) || existsSync(globalDir);
2028
2045
  });
2029
2046
  }
2047
+ function detectProjectAgents(cwd = process.cwd()) {
2048
+ return AGENTS.filter((agent) => existsSync(getAgentBasePath(agent, false, cwd)));
2049
+ }
2050
+ function preferSpecificAgents(agents) {
2051
+ const specificAgents = agents.filter((agent) => agent.id !== FALLBACK_AGENT_ID);
2052
+ return specificAgents.length > 0 ? specificAgents : agents;
2053
+ }
2054
+ function detectPreferredAgents(global, cwd = process.cwd()) {
2055
+ if (global) {
2056
+ const installedAgents = preferSpecificAgents(detectInstalledAgents());
2057
+ return installedAgents.length > 0 ? installedAgents : [getFallbackAgent()];
2058
+ }
2059
+ const projectAgents = preferSpecificAgents(detectProjectAgents(cwd));
2060
+ if (projectAgents.length > 0) {
2061
+ return projectAgents;
2062
+ }
2063
+ const installedAgents = preferSpecificAgents(detectInstalledAgents());
2064
+ return installedAgents.length > 0 ? installedAgents : [getFallbackAgent()];
2065
+ }
2030
2066
  /**
2031
2067
  * Get agent by ID
2032
2068
  */
2033
2069
  function getAgentById(id) {
2034
- return AGENTS.find(a => a.id === id || a.id === id.toLowerCase().replace(/\s+/g, '-'));
2070
+ const normalized = normalizeAgentId(id);
2071
+ return AGENTS.find((agent) => agent.id === normalized || agent.aliases?.some((alias) => normalizeAgentId(alias) === normalized));
2035
2072
  }
2036
2073
  /**
2037
2074
  * Get agents by IDs
@@ -2039,11 +2076,18 @@ function getAgentById(id) {
2039
2076
  function getAgentsByIds(ids) {
2040
2077
  return ids.map(id => getAgentById(id)).filter((a) => a !== undefined);
2041
2078
  }
2079
+ function getFallbackAgent() {
2080
+ const fallbackAgent = getAgentById(FALLBACK_AGENT_ID);
2081
+ if (!fallbackAgent) {
2082
+ throw new Error('Fallback .agents target is not configured');
2083
+ }
2084
+ return fallbackAgent;
2085
+ }
2042
2086
  /**
2043
2087
  * Get skill installation path for an agent
2044
2088
  */
2045
- function getSkillPath(agent, skillName, global) {
2046
- const basePath = global ? agent.globalPath : join(process.cwd(), agent.projectPath);
2089
+ function getSkillPath(agent, skillName, global, cwd = process.cwd()) {
2090
+ const basePath = getAgentBasePath(agent, global, cwd);
2047
2091
  return join(basePath, sanitizeSkillDirName(skillName));
2048
2092
  }
2049
2093
 
@@ -2105,6 +2149,7 @@ function normalizeSkill(raw) {
2105
2149
  sha: typeof candidate.sha === 'string' ? candidate.sha : undefined,
2106
2150
  path,
2107
2151
  contentHash: typeof candidate.contentHash === 'string' ? candidate.contentHash : undefined,
2152
+ installRoot: typeof candidate.installRoot === 'string' && candidate.installRoot ? candidate.installRoot : undefined,
2108
2153
  };
2109
2154
  }
2110
2155
  function loadDb() {
@@ -2149,11 +2194,75 @@ function sameSource(a, b) {
2149
2194
  function sameInstallationIdentity(a, b) {
2150
2195
  return (a.name === b.name &&
2151
2196
  a.global === b.global &&
2197
+ (a.installRoot ?? '') === (b.installRoot ?? '') &&
2152
2198
  a.path === b.path &&
2153
2199
  (a.registrySlug ?? '') === (b.registrySlug ?? '') &&
2154
2200
  getUpdateStrategy$1(a) === getUpdateStrategy$1(b) &&
2155
2201
  sameSource(a.source, b.source));
2156
2202
  }
2203
+ function sameLegacyProjectIdentity(existing, next) {
2204
+ return (!existing.global &&
2205
+ !next.global &&
2206
+ !existing.installRoot &&
2207
+ Boolean(next.installRoot) &&
2208
+ existing.name === next.name &&
2209
+ existing.path === next.path &&
2210
+ (existing.registrySlug ?? '') === (next.registrySlug ?? '') &&
2211
+ getUpdateStrategy$1(existing) === getUpdateStrategy$1(next) &&
2212
+ sameSource(existing.source, next.source));
2213
+ }
2214
+ function saveDbIfChanged(db, changed) {
2215
+ if (changed) {
2216
+ saveDb(db);
2217
+ }
2218
+ return db;
2219
+ }
2220
+ function reconcileInstallations(db) {
2221
+ let changed = false;
2222
+ const cwd = process.cwd();
2223
+ db.skills = db.skills.flatMap((skill) => {
2224
+ let reconciledSkill = skill;
2225
+ if (!reconciledSkill.global && !reconciledSkill.installRoot) {
2226
+ const hasCurrentWorkspaceMatch = reconciledSkill.agents.some((agentId) => {
2227
+ const agent = getAgentById(agentId);
2228
+ if (!agent) {
2229
+ return false;
2230
+ }
2231
+ const skillDir = getSkillPath(agent, reconciledSkill.name, false, cwd);
2232
+ return existsSync(join(skillDir, 'SKILL.md'));
2233
+ });
2234
+ if (!hasCurrentWorkspaceMatch) {
2235
+ return [reconciledSkill];
2236
+ }
2237
+ reconciledSkill = {
2238
+ ...reconciledSkill,
2239
+ installRoot: cwd,
2240
+ };
2241
+ changed = true;
2242
+ }
2243
+ const remainingAgents = reconciledSkill.agents.filter((agentId) => {
2244
+ const agent = getAgentById(agentId);
2245
+ if (!agent) {
2246
+ changed = true;
2247
+ return false;
2248
+ }
2249
+ const skillDir = getSkillPath(agent, reconciledSkill.name, reconciledSkill.global, reconciledSkill.installRoot);
2250
+ return existsSync(join(skillDir, 'SKILL.md'));
2251
+ });
2252
+ if (remainingAgents.length === reconciledSkill.agents.length) {
2253
+ return [reconciledSkill];
2254
+ }
2255
+ changed = true;
2256
+ if (remainingAgents.length === 0) {
2257
+ return [];
2258
+ }
2259
+ return [{
2260
+ ...reconciledSkill,
2261
+ agents: remainingAgents,
2262
+ }];
2263
+ });
2264
+ return saveDbIfChanged(db, changed);
2265
+ }
2157
2266
  /**
2158
2267
  * Record a skill installation
2159
2268
  */
@@ -2164,7 +2273,8 @@ function recordInstallation(skill) {
2164
2273
  return;
2165
2274
  }
2166
2275
  // Replace only exact same installation identity.
2167
- db.skills = db.skills.filter((existing) => !sameInstallationIdentity(existing, normalized));
2276
+ db.skills = db.skills.filter((existing) => !sameInstallationIdentity(existing, normalized) &&
2277
+ !sameLegacyProjectIdentity(existing, normalized));
2168
2278
  db.skills.push(normalized);
2169
2279
  saveDb(db);
2170
2280
  }
@@ -2186,6 +2296,9 @@ function removeInstallation(skillName, options) {
2186
2296
  if (options?.global !== undefined && skill.global !== options.global) {
2187
2297
  return [skill];
2188
2298
  }
2299
+ if (options?.installRoot !== undefined && (skill.installRoot ?? '') !== options.installRoot) {
2300
+ return [skill];
2301
+ }
2189
2302
  if (targetAgents && targetAgents.length > 0) {
2190
2303
  const remainingAgents = skill.agents.filter((agentId) => !targetAgents.includes(agentId));
2191
2304
  if (remainingAgents.length === 0) {
@@ -2197,11 +2310,60 @@ function removeInstallation(skillName, options) {
2197
2310
  });
2198
2311
  saveDb(db);
2199
2312
  }
2313
+ function copyInstallationAgent(sourceAgentId, targetAgentId, options) {
2314
+ if (sourceAgentId === targetAgentId) {
2315
+ return 0;
2316
+ }
2317
+ const db = loadDb();
2318
+ let updated = 0;
2319
+ const sourceSkillDirs = options?.sourceSkillDirs ? new Set(options.sourceSkillDirs) : null;
2320
+ const sourceAgent = getAgentById(sourceAgentId);
2321
+ db.skills = db.skills.map((skill) => {
2322
+ if (options?.global !== undefined && skill.global !== options.global) {
2323
+ return skill;
2324
+ }
2325
+ if (!skill.agents.includes(sourceAgentId) || skill.agents.includes(targetAgentId)) {
2326
+ return skill;
2327
+ }
2328
+ let installRoot = skill.installRoot;
2329
+ if (options?.installRoot !== undefined) {
2330
+ if (installRoot) {
2331
+ if (installRoot !== options.installRoot) {
2332
+ return skill;
2333
+ }
2334
+ }
2335
+ else if (!skill.global &&
2336
+ sourceAgent &&
2337
+ existsSync(join(getSkillPath(sourceAgent, skill.name, false, options.installRoot), 'SKILL.md'))) {
2338
+ installRoot = options.installRoot;
2339
+ }
2340
+ else {
2341
+ return skill;
2342
+ }
2343
+ }
2344
+ if (sourceSkillDirs && sourceAgent) {
2345
+ const sourceSkillDir = getSkillPath(sourceAgent, skill.name, skill.global, installRoot);
2346
+ if (!sourceSkillDirs.has(sourceSkillDir)) {
2347
+ return skill;
2348
+ }
2349
+ }
2350
+ updated += 1;
2351
+ return {
2352
+ ...skill,
2353
+ installRoot,
2354
+ agents: [...skill.agents, targetAgentId],
2355
+ };
2356
+ });
2357
+ if (updated > 0) {
2358
+ saveDb(db);
2359
+ }
2360
+ return updated;
2361
+ }
2200
2362
  /**
2201
2363
  * Get all installed skills
2202
2364
  */
2203
2365
  function getInstalledSkills() {
2204
- const db = loadDb();
2366
+ const db = reconcileInstallations(loadDb());
2205
2367
  return db.skills;
2206
2368
  }
2207
2369
 
@@ -2295,6 +2457,7 @@ function box(content, title) {
2295
2457
  const COMPANION_MANIFEST_FILE = '.skillscat-companion-files.json';
2296
2458
  const COMPANION_MANIFEST_VERSION = 1;
2297
2459
  async function add(source, options) {
2460
+ const explicitRepoInstall = options.repo === true;
2298
2461
  const repoSource = parseSource(source);
2299
2462
  if (!repoSource) {
2300
2463
  error('Invalid source. Supported formats:');
@@ -2316,14 +2479,18 @@ async function add(source, options) {
2316
2479
  }
2317
2480
  const discoverSpinner = spinner('Discovering skills');
2318
2481
  let resolvedSkills = [];
2482
+ let selectionMode = 'default';
2319
2483
  try {
2320
- resolvedSkills = await resolveInstallSkills({
2484
+ const resolution = await resolveInstallSkills({
2321
2485
  sourceInput: source,
2322
2486
  repoSource,
2323
2487
  requestedSkillNames: options.skill ?? [],
2488
+ explicitRepoInstall,
2324
2489
  explicitRefBypassRegistry: isExplicitGitHubRefSource,
2325
2490
  githubSnapshot,
2326
2491
  });
2492
+ resolvedSkills = resolution.resolved;
2493
+ selectionMode = resolution.selectionMode;
2327
2494
  }
2328
2495
  catch (err) {
2329
2496
  discoverSpinner.stop(false);
@@ -2360,9 +2527,28 @@ async function add(source, options) {
2360
2527
  }
2361
2528
  console.log(pc.dim('─'.repeat(50)));
2362
2529
  console.log(pc.dim('Install with:'));
2363
- console.log(` ${pc.cyan(`npx skillscat add ${source}`)}`);
2530
+ console.log(` ${pc.cyan(formatAddCommand(source, options))}`);
2364
2531
  return;
2365
2532
  }
2533
+ if (selectionMode === 'install-all' && !options.yes && (!options.skill || options.skill.length === 0)) {
2534
+ console.log();
2535
+ if (explicitRepoInstall) {
2536
+ info$1(`Treating ${pc.cyan(sourceLabel)} as a repository install.`);
2537
+ }
2538
+ else {
2539
+ info$1(`No published skill slug matched ${pc.cyan(sourceLabel)}.`);
2540
+ }
2541
+ console.log(pc.dim(`Found ${resolvedSkills.length} published skill(s) in repository ${sourceLabel}:`));
2542
+ for (const entry of resolvedSkills) {
2543
+ console.log(pc.dim(` - ${entry.skill.name}${formatSkillPathHint(entry.skill.path)}`));
2544
+ }
2545
+ console.log();
2546
+ const confirm = await prompt(`Install all ${resolvedSkills.length} skill(s) from ${sourceLabel}? [y/N] `);
2547
+ if (confirm.trim().toLowerCase() !== 'y') {
2548
+ info$1('Installation cancelled.');
2549
+ process.exit(0);
2550
+ }
2551
+ }
2366
2552
  // Final name filter (safety net after mixed registry+git resolution)
2367
2553
  let selectedEntries = resolvedSkills;
2368
2554
  if (options.skill && options.skill.length > 0) {
@@ -2376,11 +2562,13 @@ async function add(source, options) {
2376
2562
  process.exit(1);
2377
2563
  }
2378
2564
  }
2379
- else if (!options.yes && resolvedSkills.length > 1) {
2565
+ else if (selectionMode !== 'install-all' && !options.yes && resolvedSkills.length > 1) {
2380
2566
  selectedEntries = [await selectSingleSkillInteractive(resolvedSkills)];
2381
2567
  }
2382
2568
  // Detect or select agents
2383
2569
  let targetAgents;
2570
+ const isGlobal = options.global ?? false;
2571
+ let usedFallbackAgent = false;
2384
2572
  if (options.agent && options.agent.length > 0) {
2385
2573
  targetAgents = getAgentsByIds(options.agent);
2386
2574
  if (targetAgents.length === 0) {
@@ -2393,38 +2581,12 @@ async function add(source, options) {
2393
2581
  }
2394
2582
  }
2395
2583
  else {
2396
- targetAgents = detectInstalledAgents();
2397
- if (targetAgents.length === 0) {
2398
- if (!options.yes) {
2399
- console.log();
2400
- warn('No coding agents detected.');
2401
- console.log(pc.dim('Select agents to install skills for:'));
2402
- console.log();
2403
- for (let i = 0; i < AGENTS.length; i++) {
2404
- console.log(` ${pc.dim(`${i + 1}.`)} ${AGENTS[i].name} (${AGENTS[i].id})`);
2405
- }
2406
- console.log();
2407
- const response = await prompt('Enter agent numbers (comma-separated) or "all": ');
2408
- if (response.toLowerCase() === 'all') {
2409
- targetAgents = AGENTS;
2410
- }
2411
- else {
2412
- const indices = response.split(',').map((s) => parseInt(s.trim()) - 1);
2413
- targetAgents = indices
2414
- .filter((i) => i >= 0 && i < AGENTS.length)
2415
- .map((i) => AGENTS[i]);
2416
- }
2417
- if (targetAgents.length === 0) {
2418
- error('No agents selected.');
2419
- process.exit(1);
2420
- }
2421
- }
2422
- else {
2423
- targetAgents = AGENTS.filter((a) => a.id === 'claude-code');
2424
- }
2584
+ targetAgents = detectPreferredAgents(isGlobal);
2585
+ usedFallbackAgent = targetAgents.length === 1 && targetAgents[0]?.id === FALLBACK_AGENT_ID;
2586
+ if (usedFallbackAgent) {
2587
+ warn('No agent-specific directory detected. Installing to .agents.');
2425
2588
  }
2426
2589
  }
2427
- const isGlobal = options.global ?? false;
2428
2590
  const locationLabel = isGlobal ? 'global' : 'project';
2429
2591
  console.log();
2430
2592
  console.log(pc.bold(`Installing ${selectedEntries.length} skill(s) to ${targetAgents.length} agent(s):`));
@@ -2437,7 +2599,7 @@ async function add(source, options) {
2437
2599
  console.log();
2438
2600
  console.log(pc.dim('Target agents:'));
2439
2601
  for (const agent of targetAgents) {
2440
- const path = isGlobal ? agent.globalPath : join(process.cwd(), agent.projectPath);
2602
+ const path = getAgentBasePath(agent, isGlobal);
2441
2603
  console.log(` ${pc.cyan('•')} ${agent.name} → ${pc.dim(path)}`);
2442
2604
  }
2443
2605
  console.log();
@@ -2523,6 +2685,7 @@ async function add(source, options) {
2523
2685
  sha: skill.sha,
2524
2686
  path: skill.path,
2525
2687
  contentHash: skill.contentHash,
2688
+ installRoot: isGlobal ? undefined : process.cwd(),
2526
2689
  });
2527
2690
  trackInstallation(entry.trackingSlug).catch(() => { });
2528
2691
  }
@@ -2544,13 +2707,32 @@ async function add(source, options) {
2544
2707
  console.log();
2545
2708
  console.log(pc.dim('Skills are now available in your coding agents.'));
2546
2709
  console.log(pc.dim('Restart your agent or start a new session to use them.'));
2710
+ if (usedFallbackAgent) {
2711
+ console.log(pc.dim('Need a tool-specific copy later? Run `npx skillscat convert <agent>` to copy from .agents.'));
2712
+ }
2547
2713
  }
2548
- async function resolveInstallSkills({ sourceInput, repoSource, requestedSkillNames, explicitRefBypassRegistry, githubSnapshot, }) {
2714
+ async function resolveInstallSkills({ sourceInput, repoSource, requestedSkillNames, explicitRepoInstall, explicitRefBypassRegistry, githubSnapshot, }) {
2549
2715
  const requestedNamesLower = new Set(requestedSkillNames.map((name) => name.toLowerCase()));
2550
2716
  const needsRegistryFirst = repoSource.platform === 'github' && !explicitRefBypassRegistry;
2717
+ const ambiguousSlugOrRepoInput = isAmbiguousSlugOrRepoInput(sourceInput, repoSource);
2718
+ const preferRepoSelection = explicitRepoInstall || ambiguousSlugOrRepoInput;
2719
+ const canShortCircuitExactSlug = ambiguousSlugOrRepoInput && !explicitRepoInstall && requestedNamesLower.size === 0;
2551
2720
  let resolved = [];
2721
+ let selectionMode = 'default';
2552
2722
  let registryRepoMatchesFound = false;
2553
2723
  let registrySummariesNeedingGitBackfill = [];
2724
+ if (canShortCircuitExactSlug) {
2725
+ const registrySlugSkill = await fetchSkill(sourceInput).catch((err) => {
2726
+ verboseLog(`Registry slug lookup failed: ${err instanceof Error ? err.message : 'unknown'}`);
2727
+ return null;
2728
+ });
2729
+ if (registrySlugSkill?.content) {
2730
+ return {
2731
+ resolved: [toRegistryResolvedSkill(registrySlugSkill, sourceInput, repoSource)],
2732
+ selectionMode,
2733
+ };
2734
+ }
2735
+ }
2554
2736
  if (needsRegistryFirst) {
2555
2737
  try {
2556
2738
  const registryRepo = await fetchSkillsByRepo(repoSource.owner, repoSource.repo, {
@@ -2567,6 +2749,9 @@ async function resolveInstallSkills({ sourceInput, repoSource, requestedSkillNam
2567
2749
  if (requestedNamesLower.size > 0) {
2568
2750
  selectedSummaries = summaries.filter((item) => requestedNamesLower.has(item.name.toLowerCase()));
2569
2751
  }
2752
+ else if (preferRepoSelection) {
2753
+ selectionMode = 'install-all';
2754
+ }
2570
2755
  if (selectedSummaries.length > 0) {
2571
2756
  const registryFetch = await fetchRegistryResolvedSkills(selectedSummaries, sourceInput, repoSource);
2572
2757
  resolved.push(...registryFetch.resolved);
@@ -2608,11 +2793,14 @@ async function resolveInstallSkills({ sourceInput, repoSource, requestedSkillNam
2608
2793
  throw err;
2609
2794
  }
2610
2795
  // Fallback: preserve existing behavior for registry slugs or private skills.
2611
- if (resolved.length === 0 && true) {
2796
+ if (!explicitRepoInstall && resolved.length === 0 && true) {
2612
2797
  const registrySkill = await fetchSkill(sourceInput).catch(() => null);
2613
2798
  if (registrySkill?.content) {
2614
2799
  resolved.push(toRegistryResolvedSkill(registrySkill, sourceInput, repoSource));
2615
- return mergeResolvedSkills([], resolved);
2800
+ return {
2801
+ resolved: mergeResolvedSkills([], resolved),
2802
+ selectionMode,
2803
+ };
2616
2804
  }
2617
2805
  }
2618
2806
  // If some registry skills were already resolved (partial hit), keep them.
@@ -2620,7 +2808,10 @@ async function resolveInstallSkills({ sourceInput, repoSource, requestedSkillNam
2620
2808
  if (getMissingRequestedNames(requestedSkillNames, resolved).length > 0) {
2621
2809
  throw err;
2622
2810
  }
2623
- return mergeResolvedSkills([], resolved);
2811
+ return {
2812
+ resolved: mergeResolvedSkills([], resolved),
2813
+ selectionMode,
2814
+ };
2624
2815
  }
2625
2816
  throw err;
2626
2817
  }
@@ -2628,7 +2819,10 @@ async function resolveInstallSkills({ sourceInput, repoSource, requestedSkillNam
2628
2819
  if (requestedNamesLower.size > 0 && resolved.length > 0) {
2629
2820
  resolved = resolved.filter((entry) => requestedNamesLower.has(entry.skill.name.toLowerCase()));
2630
2821
  }
2631
- return mergeResolvedSkills([], resolved);
2822
+ return {
2823
+ resolved: mergeResolvedSkills([], resolved),
2824
+ selectionMode,
2825
+ };
2632
2826
  }
2633
2827
  async function fetchRegistryResolvedSkills(summaries, sourceInput, repoSource) {
2634
2828
  const resolved = [];
@@ -2917,10 +3111,24 @@ function normalizeSkillPath(path) {
2917
3111
  return '';
2918
3112
  return normalized.replace(/(?:^|\/)SKILL\.md$/i, '');
2919
3113
  }
3114
+ function formatSkillPathHint(path) {
3115
+ const normalized = normalizeSkillPath(path);
3116
+ return normalized ? ` (${normalized})` : '';
3117
+ }
3118
+ function formatAddCommand(source, options) {
3119
+ return options.repo ? `npx skillscat add ${source} --repo` : `npx skillscat add ${source}`;
3120
+ }
2920
3121
  function toSkillFilePath(path) {
2921
3122
  const normalized = normalizeSkillPath(path);
2922
3123
  return normalized ? `${normalized}/SKILL.md` : 'SKILL.md';
2923
3124
  }
3125
+ function isAmbiguousSlugOrRepoInput(sourceInput, repoSource) {
3126
+ return (repoSource.platform === 'github'
3127
+ && !repoSource.path
3128
+ && !repoSource.branch
3129
+ && repoSource.hasExplicitRef !== true
3130
+ && sourceInput.trim() === `${repoSource.owner}/${repoSource.repo}`);
3131
+ }
2924
3132
  function getSourceFromRegistrySkill(skill) {
2925
3133
  if (skill.githubUrl) {
2926
3134
  const source = parseSource(skill.githubUrl);
@@ -3034,7 +3242,7 @@ async function list(options) {
3034
3242
  warn('No skills installed.');
3035
3243
  console.log();
3036
3244
  console.log(pc.dim('Install skills with:'));
3037
- console.log(` ${pc.cyan('npx skillscat add <owner>/<repo>')}`);
3245
+ console.log(` ${pc.cyan('npx skillscat add <slug>')}`);
3038
3246
  console.log();
3039
3247
  console.log(pc.dim('Or search for skills:'));
3040
3248
  console.log(` ${pc.cyan('npx skillscat search <query>')}`);
@@ -3168,13 +3376,15 @@ async function search(query, options = {}) {
3168
3376
  console.log(pc.dim('─'.repeat(50)));
3169
3377
  console.log();
3170
3378
  console.log(pc.dim('Install a skill:'));
3171
- console.log(` ${pc.cyan('npx skillscat add <owner>/<repo>')}`);
3379
+ console.log(` ${pc.cyan('npx skillscat add <slug>')}`);
3172
3380
  console.log();
3173
- console.log(pc.dim('View skill details:'));
3381
+ console.log(pc.dim('Inspect a repository first:'));
3174
3382
  console.log(` ${pc.cyan('npx skillscat info <owner>/<repo>')}`);
3175
3383
  }
3176
3384
 
3177
3385
  async function remove(skillName, options) {
3386
+ // Reconcile tracked installs so manual deletions do not leave stale records behind.
3387
+ getInstalledSkills();
3178
3388
  // Determine which agents to check
3179
3389
  let agents;
3180
3390
  if (options.agent && options.agent.length > 0) {
@@ -3209,6 +3419,7 @@ async function remove(skillName, options) {
3209
3419
  removeInstallation(skillName, {
3210
3420
  agents: agents.map((agent) => agent.id),
3211
3421
  global: isGlobal,
3422
+ installRoot: isGlobal ? undefined : process.cwd(),
3212
3423
  });
3213
3424
  }
3214
3425
  if (removed === 0) {
@@ -3382,7 +3593,7 @@ async function update(skillName, options) {
3382
3593
  skillAgents.push(...agents.filter(a => skill.agents.includes(a.id)));
3383
3594
  }
3384
3595
  for (const agent of skillAgents) {
3385
- const skillDir = getSkillPath(agent, skill.name, skill.global);
3596
+ const skillDir = getSkillPath(agent, skill.name, skill.global, skill.installRoot);
3386
3597
  const skillFile = join(skillDir, 'SKILL.md');
3387
3598
  try {
3388
3599
  mkdirSync(dirname(skillFile), { recursive: true });
@@ -3410,6 +3621,78 @@ async function update(skillName, options) {
3410
3621
  success(`Updated ${updated} skill(s) successfully!`);
3411
3622
  }
3412
3623
 
3624
+ function getSkillDirectories(basePath) {
3625
+ if (!existsSync(basePath)) {
3626
+ return [];
3627
+ }
3628
+ return readdirSync(basePath, { withFileTypes: true })
3629
+ .filter((entry) => entry.isDirectory())
3630
+ .map((entry) => join(basePath, entry.name))
3631
+ .filter((skillDir) => existsSync(join(skillDir, 'SKILL.md')));
3632
+ }
3633
+ async function convert(targetAgentId, options) {
3634
+ const isGlobal = options.global ?? false;
3635
+ const sourceAgent = getAgentById(options.from ?? FALLBACK_AGENT_ID);
3636
+ const targetAgent = getAgentById(targetAgentId);
3637
+ if (!sourceAgent) {
3638
+ error(`Invalid source agent: ${options.from ?? FALLBACK_AGENT_ID}`);
3639
+ process.exit(1);
3640
+ }
3641
+ if (!targetAgent) {
3642
+ error(`Invalid target agent: ${targetAgentId}`);
3643
+ process.exit(1);
3644
+ }
3645
+ const sourceBase = getAgentBasePath(sourceAgent, isGlobal);
3646
+ const targetBase = getAgentBasePath(targetAgent, isGlobal);
3647
+ if (sourceBase === targetBase) {
3648
+ error('Source and target directories are the same.');
3649
+ process.exit(1);
3650
+ }
3651
+ const sourceSkills = getSkillDirectories(sourceBase);
3652
+ if (sourceSkills.length === 0) {
3653
+ warn(`No skills found in ${sourceAgent.name}.`);
3654
+ console.log(pc.dim(`Checked: ${sourceBase}`));
3655
+ return;
3656
+ }
3657
+ let copied = 0;
3658
+ let skipped = 0;
3659
+ const copiedSkillDirs = [];
3660
+ for (const sourceSkillDir of sourceSkills) {
3661
+ const skillName = basename(sourceSkillDir);
3662
+ const targetSkillDir = join(targetBase, skillName);
3663
+ if (existsSync(targetSkillDir)) {
3664
+ if (!options.force) {
3665
+ skipped += 1;
3666
+ continue;
3667
+ }
3668
+ rmSync(targetSkillDir, { recursive: true, force: true });
3669
+ }
3670
+ mkdirSync(dirname(targetSkillDir), { recursive: true });
3671
+ cpSync(sourceSkillDir, targetSkillDir, { recursive: true });
3672
+ copied += 1;
3673
+ copiedSkillDirs.push(sourceSkillDir);
3674
+ }
3675
+ if (copied === 0) {
3676
+ warn(`All ${sourceSkills.length} skill(s) already exist in ${targetAgent.name}.`);
3677
+ console.log(pc.dim('Use `--force` to overwrite the target copy.'));
3678
+ return;
3679
+ }
3680
+ const trackedUpdates = copyInstallationAgent(sourceAgent.id, targetAgent.id, {
3681
+ global: isGlobal,
3682
+ installRoot: isGlobal ? undefined : process.cwd(),
3683
+ sourceSkillDirs: copiedSkillDirs,
3684
+ });
3685
+ console.log();
3686
+ success(`Copied ${copied} skill(s) from ${sourceAgent.name} to ${targetAgent.name}.`);
3687
+ console.log(pc.dim(`${sourceBase} → ${targetBase}`));
3688
+ if (skipped > 0) {
3689
+ info$1(`Skipped ${skipped} existing skill(s). Use --force to overwrite them.`);
3690
+ }
3691
+ if (trackedUpdates > 0) {
3692
+ console.log(pc.dim(`Updated ${trackedUpdates} tracked installation(s) so future updates also target ${targetAgent.name}.`));
3693
+ }
3694
+ }
3695
+
3413
3696
  const PACKAGE_NAME = 'skillscat';
3414
3697
  function safeExec(command, args) {
3415
3698
  try {
@@ -3640,8 +3923,9 @@ async function info(source) {
3640
3923
  }
3641
3924
  console.log(pc.dim('─'.repeat(50)));
3642
3925
  console.log();
3643
- console.log(pc.bold('Install:'));
3644
- console.log(` ${pc.cyan(`npx skillscat add ${source}`)}`);
3926
+ console.log(pc.bold('Install published skills by slug:'));
3927
+ console.log(` ${pc.cyan('npx skillscat add <slug>')}`);
3928
+ console.log(` ${pc.dim('Use search or the web detail page to discover the exact published slug for this repository.')}`);
3645
3929
  console.log();
3646
3930
  // Show compatible agents
3647
3931
  console.log(pc.bold('Compatible agents:'));
@@ -4092,6 +4376,14 @@ async function publish(skillPath, options) {
4092
4376
  }
4093
4377
  }
4094
4378
 
4379
+ function printSubmitMessage(message) {
4380
+ for (const line of message.split('\n')) {
4381
+ console.log(pc.dim(line));
4382
+ }
4383
+ }
4384
+ const IGNORED_DISCOVERY_DIRS = new Set([
4385
+ '.git',
4386
+ ]);
4095
4387
  /**
4096
4388
  * Check if a URL is a valid GitHub URL
4097
4389
  */
@@ -4182,8 +4474,7 @@ function findSkillMd(cwd, maxDepth = 3) {
4182
4474
  try {
4183
4475
  const entries = readdirSync(dir);
4184
4476
  for (const entry of entries) {
4185
- // Skip dot folders
4186
- if (entry.startsWith('.'))
4477
+ if (IGNORED_DISCOVERY_DIRS.has(entry))
4187
4478
  continue;
4188
4479
  const fullPath = join(dir, entry);
4189
4480
  try {
@@ -4364,26 +4655,46 @@ async function submit(urlArg, _options) {
4364
4655
  process.exit(1);
4365
4656
  }
4366
4657
  if (response.status === 400) {
4367
- console.error(pc.red(`Submission failed: ${result.error || 'Invalid request'}`));
4368
- if (result.error?.includes('SKILL.md')) {
4658
+ const submitError = result.error || result.message || 'Invalid request';
4659
+ console.error(pc.red(`Submission failed: ${submitError}`));
4660
+ if (result.code === 'no_skill_md_found') {
4369
4661
  console.log();
4370
- console.log(pc.dim('Make sure your repository has a SKILL.md file in the root directory.'));
4662
+ console.log(pc.dim('Make sure your repository has a SKILL.md file in the repository root or a supported subdirectory, including dot folders.'));
4371
4663
  console.log(pc.dim('Learn more: https://skillscat.com/docs/skill-format'));
4372
4664
  }
4373
- if (result.error?.includes('fork') || result.error?.includes('Fork')) {
4665
+ if (result.code === 'fork_no_unique_commits') {
4374
4666
  console.log();
4375
- console.log(pc.dim('Please submit the original repository instead of a fork.'));
4667
+ console.log(pc.dim('Please submit the original repository, or add your own commits to the fork first.'));
4668
+ }
4669
+ if (result.code === 'fork_behind_upstream') {
4670
+ console.log();
4671
+ console.log(pc.dim('Please sync your fork with upstream before submitting it.'));
4376
4672
  }
4377
4673
  process.exit(1);
4378
4674
  }
4379
4675
  if (!response.ok || !result.success) {
4380
4676
  console.error(pc.red(`Submission failed: ${result.error || result.message || 'Unknown error'}`));
4677
+ if (result.code === 'github_rate_limited' && result.retryAfterSeconds) {
4678
+ console.log(pc.dim(`Try again in about ${result.retryAfterSeconds} seconds.`));
4679
+ }
4381
4680
  process.exit(1);
4382
4681
  }
4383
4682
  // Success!
4683
+ const submittedCount = result.submitted ?? 0;
4684
+ const existingCount = result.existing ?? 0;
4384
4685
  console.log();
4385
- console.log(pc.green('Skill submitted successfully!'));
4386
- console.log(pc.dim(result.message || 'It will appear in the catalog once processed.'));
4686
+ if (existingCount > 0 && submittedCount === 0) {
4687
+ console.log(pc.green('No new submission needed.'));
4688
+ }
4689
+ else {
4690
+ console.log(pc.green('Skill submitted successfully!'));
4691
+ }
4692
+ printSubmitMessage(result.message || 'It will appear in the catalog once processed.');
4693
+ if (result.existingSlug) {
4694
+ console.log();
4695
+ console.log(`View it at: ${pc.cyan(`${baseUrl}/skills/${result.existingSlug}`)}`);
4696
+ console.log(`Install with: ${pc.cyan(`skillscat add ${result.existingSlug}`)}`);
4697
+ }
4387
4698
  }
4388
4699
  catch (error) {
4389
4700
  console.error(pc.red('Failed to connect to SkillsCat.'));
@@ -4394,6 +4705,60 @@ async function submit(urlArg, _options) {
4394
4705
  }
4395
4706
  }
4396
4707
 
4708
+ function normalizeReason(value) {
4709
+ if (!value)
4710
+ return null;
4711
+ const normalized = value.trim().toLowerCase();
4712
+ if (normalized === 'security' || normalized === 'copyright') {
4713
+ return normalized;
4714
+ }
4715
+ return null;
4716
+ }
4717
+ async function report(slug, options = {}) {
4718
+ const token = await getValidToken();
4719
+ if (!token) {
4720
+ error('Authentication required.');
4721
+ console.log(pc.dim('Run `skillscat login` first.'));
4722
+ process.exit(1);
4723
+ }
4724
+ let reason = normalizeReason(options.reason);
4725
+ if (!reason) {
4726
+ const answer = await prompt('Report reason [security/copyright]: ');
4727
+ reason = normalizeReason(answer);
4728
+ }
4729
+ if (!reason) {
4730
+ error('Invalid report reason. Use `security` or `copyright`.');
4731
+ process.exit(1);
4732
+ }
4733
+ const baseUrl = getBaseUrl();
4734
+ const response = await fetch(`${baseUrl}/api/skills/${encodeURIComponent(slug)}/report`, {
4735
+ method: 'POST',
4736
+ headers: {
4737
+ Authorization: `Bearer ${token}`,
4738
+ 'Content-Type': 'application/json',
4739
+ Origin: baseUrl,
4740
+ 'User-Agent': 'skillscat-cli/0.1.0',
4741
+ },
4742
+ body: JSON.stringify({
4743
+ reason,
4744
+ details: options.details || undefined,
4745
+ }),
4746
+ });
4747
+ const payload = await response.json();
4748
+ if (!response.ok || !payload.success) {
4749
+ error(payload.error || payload.message || 'Failed to submit report');
4750
+ process.exit(1);
4751
+ }
4752
+ success(payload.message || 'Report submitted');
4753
+ if (reason === 'security' && payload.report) {
4754
+ console.log(` Open security reports: ${pc.cyan(String(payload.report.openSecurityReportCount))}`);
4755
+ console.log(` Risk level: ${pc.cyan(payload.report.riskLevel)}`);
4756
+ if (payload.report.premiumEscalated) {
4757
+ console.log(` ${pc.yellow('Premium review queued')}`);
4758
+ }
4759
+ }
4760
+ }
4761
+
4397
4762
  /**
4398
4763
  * Find skill by slug using two-segment path
4399
4764
  */
@@ -4487,6 +4852,208 @@ async function unpublishSkill(slug, options) {
4487
4852
  }
4488
4853
  }
4489
4854
 
4855
+ function hasCommand(command) {
4856
+ const trimmed = command.trim();
4857
+ if (!trimmed) {
4858
+ return false;
4859
+ }
4860
+ const lookupCommand = process.platform === 'win32' ? 'where' : 'which';
4861
+ const result = spawnSync(lookupCommand, [trimmed], {
4862
+ stdio: 'ignore',
4863
+ });
4864
+ return result.status === 0;
4865
+ }
4866
+ function getEnvBrowserCommand(url) {
4867
+ const rawBrowser = process.env.BROWSER?.trim();
4868
+ if (!rawBrowser || rawBrowser.toLowerCase() === 'none') {
4869
+ return null;
4870
+ }
4871
+ const [command, ...rawArgs] = rawBrowser.split(/\s+/);
4872
+ if (!command) {
4873
+ return null;
4874
+ }
4875
+ let replacedUrl = false;
4876
+ const args = rawArgs.map((arg) => {
4877
+ if (!arg.includes('%s')) {
4878
+ return arg;
4879
+ }
4880
+ replacedUrl = true;
4881
+ return arg.replace(/%s/g, url);
4882
+ });
4883
+ if (!replacedUrl) {
4884
+ args.push(url);
4885
+ }
4886
+ return { command, args };
4887
+ }
4888
+ function getDefaultBrowserCommand(url) {
4889
+ if (process.platform === 'darwin') {
4890
+ return { command: 'open', args: [url] };
4891
+ }
4892
+ if (process.platform === 'win32') {
4893
+ return { command: 'rundll32', args: ['url.dll,FileProtocolHandler', url] };
4894
+ }
4895
+ if ((process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) && hasCommand('wslview')) {
4896
+ return { command: 'wslview', args: [url] };
4897
+ }
4898
+ return { command: 'xdg-open', args: [url] };
4899
+ }
4900
+ function getBrowserCommand(url) {
4901
+ return getEnvBrowserCommand(url) || getDefaultBrowserCommand(url);
4902
+ }
4903
+ function hasBrowserEnvironment() {
4904
+ const rawBrowser = process.env.BROWSER?.trim();
4905
+ if (rawBrowser?.toLowerCase() === 'none') {
4906
+ return false;
4907
+ }
4908
+ if (rawBrowser) {
4909
+ return true;
4910
+ }
4911
+ if (process.env.CI) {
4912
+ return false;
4913
+ }
4914
+ if (process.platform === 'linux') {
4915
+ if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) {
4916
+ return true;
4917
+ }
4918
+ return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
4919
+ }
4920
+ return true;
4921
+ }
4922
+ function canOpenUrlInBrowser() {
4923
+ if (!hasBrowserEnvironment()) {
4924
+ return false;
4925
+ }
4926
+ return hasCommand(getBrowserCommand('https://skills.cat').command);
4927
+ }
4928
+ async function openUrlInBrowser(url) {
4929
+ if (!canOpenUrlInBrowser()) {
4930
+ return false;
4931
+ }
4932
+ const browserCommand = getBrowserCommand(url);
4933
+ return new Promise((resolve) => {
4934
+ execFile(browserCommand.command, browserCommand.args, (err) => {
4935
+ resolve(!err);
4936
+ });
4937
+ });
4938
+ }
4939
+
4940
+ class ViewHttpError extends Error {
4941
+ }
4942
+ function normalizeOutputFormat(output) {
4943
+ if (!output) {
4944
+ return null;
4945
+ }
4946
+ const normalized = output.trim().toLowerCase();
4947
+ if (normalized === 'html' || normalized === 'markdown') {
4948
+ return normalized;
4949
+ }
4950
+ return null;
4951
+ }
4952
+ function getSiteBaseUrl() {
4953
+ const registryUrl = getRegistryUrl();
4954
+ try {
4955
+ const parsed = new URL(registryUrl);
4956
+ const pathname = parsed.pathname.replace(/\/+$/, '');
4957
+ const strippedPathname = pathname.endsWith('/registry')
4958
+ ? pathname.slice(0, -'/registry'.length)
4959
+ : pathname.endsWith('/openclaw')
4960
+ ? pathname.slice(0, -'/openclaw'.length)
4961
+ : pathname;
4962
+ return `${parsed.protocol}//${parsed.host}${strippedPathname}`;
4963
+ }
4964
+ catch {
4965
+ return registryUrl.replace(/\/(?:registry|openclaw)\/?$/, '');
4966
+ }
4967
+ }
4968
+ function buildSkillUrl(slug) {
4969
+ return `${getSiteBaseUrl()}${buildSkillPath(slug)}`;
4970
+ }
4971
+ async function buildViewHeaders(output) {
4972
+ const token = await getValidToken();
4973
+ const headers = {
4974
+ 'User-Agent': output === 'markdown'
4975
+ ? 'skillscat-cli/0.1.0 OpenClaw/1.0'
4976
+ : 'skillscat-cli/0.1.0',
4977
+ Accept: output === 'markdown'
4978
+ ? 'text/markdown, text/plain;q=0.9, */*;q=0.8'
4979
+ : 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.8',
4980
+ };
4981
+ if (token) {
4982
+ headers.Authorization = `Bearer ${token}`;
4983
+ }
4984
+ return headers;
4985
+ }
4986
+ async function fetchSkillDocument(slug, output) {
4987
+ const url = buildSkillUrl(slug);
4988
+ const headers = await buildViewHeaders(output);
4989
+ const startTime = Date.now();
4990
+ verboseRequest('GET', url, headers);
4991
+ try {
4992
+ const response = await fetch(url, { headers });
4993
+ verboseResponse(response.status, response.statusText, Date.now() - startTime);
4994
+ if (!response.ok) {
4995
+ const httpError = parseHttpError(response.status, response.statusText);
4996
+ throw new ViewHttpError(httpError.message);
4997
+ }
4998
+ return response.text();
4999
+ }
5000
+ catch (err) {
5001
+ if (!(err instanceof ViewHttpError)) {
5002
+ const networkError = parseNetworkError(err);
5003
+ throw new Error(networkError.message);
5004
+ }
5005
+ throw err;
5006
+ }
5007
+ }
5008
+ function writeOutput(body) {
5009
+ console.log(body);
5010
+ }
5011
+ async function view(slug, options = {}) {
5012
+ try {
5013
+ parseSlug(slug);
5014
+ }
5015
+ catch {
5016
+ error('Invalid slug. Expected format: owner/name');
5017
+ process.exit(1);
5018
+ }
5019
+ if (options.output) {
5020
+ const output = normalizeOutputFormat(options.output);
5021
+ if (!output) {
5022
+ error('Invalid output format. Use `html` or `markdown`.');
5023
+ process.exit(1);
5024
+ }
5025
+ try {
5026
+ const body = await fetchSkillDocument(slug, output);
5027
+ writeOutput(body);
5028
+ return;
5029
+ }
5030
+ catch (err) {
5031
+ error(err instanceof Error ? err.message : 'Failed to fetch skill view');
5032
+ process.exit(1);
5033
+ }
5034
+ }
5035
+ const url = buildSkillUrl(slug);
5036
+ if (canOpenUrlInBrowser()) {
5037
+ const opened = await openUrlInBrowser(url);
5038
+ if (opened) {
5039
+ info$1(`Opened ${slug} in your browser.`);
5040
+ return;
5041
+ }
5042
+ warn('Unable to open a browser automatically. Printing markdown instead.');
5043
+ }
5044
+ else {
5045
+ warn('No browser environment detected. Printing markdown instead.');
5046
+ }
5047
+ try {
5048
+ const markdown = await fetchSkillDocument(slug, 'markdown');
5049
+ writeOutput(markdown);
5050
+ }
5051
+ catch (err) {
5052
+ error(err instanceof Error ? err.message : 'Failed to fetch skill view');
5053
+ process.exit(1);
5054
+ }
5055
+ }
5056
+
4490
5057
  const DEFAULT_REGISTRY_URL = 'https://skills.cat/registry';
4491
5058
  const VALID_KEYS = ['registry'];
4492
5059
  async function configSet(key, value, options = {}) {
@@ -4578,7 +5145,8 @@ program
4578
5145
  .alias('i')
4579
5146
  .description('Add a skill from a repository')
4580
5147
  .option('-g, --global', 'Install to user directory instead of project')
4581
- .option('-a, --agent <agents...>', 'Target specific agents (e.g., claude-code, cursor)')
5148
+ .option('-a, --agent <agents...>', 'Target specific agents (e.g., claude-code, cursor, agents)')
5149
+ .option('-r, --repo', 'Treat <source> as a repository instead of an exact published skill slug')
4582
5150
  .option('-s, --skill <skills...>', 'Install specific skills by name')
4583
5151
  .option('-l, --list', 'List available skills without installing')
4584
5152
  .option('-y, --yes', 'Skip confirmation prompts')
@@ -4610,6 +5178,14 @@ program
4610
5178
  .option('-a, --agent <agents...>', 'Update for specific agents')
4611
5179
  .option('--check', 'Check for updates without installing')
4612
5180
  .action(update);
5181
+ program
5182
+ .command('convert <agent>')
5183
+ .alias('sync')
5184
+ .description('Copy skills from .agents into a specific agent directory')
5185
+ .option('--from <agent>', 'Source agent directory to copy from', 'agents')
5186
+ .option('-g, --global', 'Copy from user directory instead of project')
5187
+ .option('-f, --force', 'Overwrite existing skills in the target directory')
5188
+ .action(convert);
4613
5189
  // Self-upgrade command
4614
5190
  program
4615
5191
  .command('self-upgrade')
@@ -4629,6 +5205,12 @@ program
4629
5205
  .command('info <source>')
4630
5206
  .description('Show detailed information about a skill or repository')
4631
5207
  .action(info);
5208
+ // View command
5209
+ program
5210
+ .command('view <slug>')
5211
+ .description('Open a published skill in the browser or print its rendered output')
5212
+ .option('-o, --output <format>', 'Print `html` or `markdown` to stdout instead of opening a browser')
5213
+ .action(view);
4632
5214
  // Login command
4633
5215
  program
4634
5216
  .command('login')
@@ -4660,6 +5242,12 @@ program
4660
5242
  .command('submit [url]')
4661
5243
  .description('Submit a GitHub repository to SkillsCat registry')
4662
5244
  .action(submit);
5245
+ program
5246
+ .command('report <slug>')
5247
+ .description('Report a skill for security or copyright concerns')
5248
+ .option('-r, --reason <reason>', 'Report reason (security or copyright)')
5249
+ .option('-d, --details <details>', 'Optional report details')
5250
+ .action(report);
4663
5251
  // Unpublish command
4664
5252
  program
4665
5253
  .command('unpublish <slug>')