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/commands/add.d.ts +1 -0
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/convert.d.ts +8 -0
- package/dist/commands/convert.d.ts.map +1 -0
- package/dist/commands/info.d.ts.map +1 -1
- package/dist/commands/remove.d.ts.map +1 -1
- package/dist/commands/report.d.ts +7 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/submit.d.ts.map +1 -1
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.d.ts.map +1 -0
- package/dist/index.js +683 -95
- package/dist/utils/agents/agents.d.ts +7 -1
- package/dist/utils/agents/agents.d.ts.map +1 -1
- package/dist/utils/core/browser.d.ts +3 -0
- package/dist/utils/core/browser.d.ts.map +1 -0
- package/dist/utils/core/slug.d.ts +12 -0
- package/dist/utils/core/slug.d.ts.map +1 -1
- package/dist/utils/source/git.d.ts.map +1 -1
- package/dist/utils/source/source.d.ts.map +1 -1
- package/dist/utils/storage/db.d.ts +7 -0
- package/dist/utils/storage/db.d.ts.map +1 -1
- package/package.json +5 -2
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
|
-
|
|
2026
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 <
|
|
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 <
|
|
3379
|
+
console.log(` ${pc.cyan('npx skillscat add <slug>')}`);
|
|
3172
3380
|
console.log();
|
|
3173
|
-
console.log(pc.dim('
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
4368
|
-
|
|
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
|
|
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.
|
|
4665
|
+
if (result.code === 'fork_no_unique_commits') {
|
|
4374
4666
|
console.log();
|
|
4375
|
-
console.log(pc.dim('Please submit the original repository
|
|
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
|
-
|
|
4386
|
-
|
|
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>')
|