skillcaddy 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +497 -0
  2. package/CONTEXT.md +149 -0
  3. package/LICENSE +21 -0
  4. package/README.md +499 -0
  5. package/README_CN.md +482 -0
  6. package/collection-metadata/github/mattpocock.json +16 -0
  7. package/lib/canonicalJson.js +11 -0
  8. package/lib/claudeStore.js +135 -0
  9. package/lib/collectionEnablement.js +115 -0
  10. package/lib/collectionSetup.js +199 -0
  11. package/lib/enablePlan.js +116 -0
  12. package/lib/enablementScope.js +8 -0
  13. package/lib/hermesStore.js +21 -0
  14. package/lib/libraryImage.js +1785 -0
  15. package/lib/managerInstall.js +109 -0
  16. package/lib/managerSourceWorkflow.js +234 -0
  17. package/lib/projectActions.js +24 -0
  18. package/lib/projectPath.js +51 -0
  19. package/lib/roots.js +99 -0
  20. package/lib/skillLinkStore.js +68 -0
  21. package/lib/skillMetadata.js +191 -0
  22. package/lib/skillStore.js +541 -0
  23. package/lib/sourceAcquisition.js +475 -0
  24. package/lib/sourceAcquisitionAdapter.js +47 -0
  25. package/lib/sourceAcquisitionError.js +18 -0
  26. package/lib/sourceAcquisitionPolicy.js +139 -0
  27. package/lib/sourceArchive.js +363 -0
  28. package/lib/sourceArchiveWorkspace.js +36 -0
  29. package/lib/sourceGit.js +95 -0
  30. package/lib/sourceGitAcquisitionAdapter.js +145 -0
  31. package/lib/sourceGitBatchUpdate.js +168 -0
  32. package/lib/sourceGitRepair.js +227 -0
  33. package/lib/sourceGitStaging.js +139 -0
  34. package/lib/sourceGitUpgradeAdapter.js +583 -0
  35. package/lib/sourceGitUrl.js +170 -0
  36. package/lib/sourceHttp.js +302 -0
  37. package/lib/sourceLocal.js +200 -0
  38. package/lib/sourceLocalAcquisitionAdapter.js +107 -0
  39. package/lib/sourceManager.js +242 -0
  40. package/lib/sourceMigration.js +172 -0
  41. package/lib/sourceMigrationIssue.js +6 -0
  42. package/lib/sourcePath.js +6 -0
  43. package/lib/sourcePolicy.js +53 -0
  44. package/lib/sourceRecord.js +295 -0
  45. package/lib/sourceRegistry.js +155 -0
  46. package/lib/sourceRemoteArchiveAcquisitionAdapter.js +147 -0
  47. package/lib/sourceRemoteFileAcquisitionAdapter.js +95 -0
  48. package/lib/sourceRemoteFilePolicy.js +62 -0
  49. package/lib/sourceRemoteFileWorkspace.js +55 -0
  50. package/lib/sourceSkillDiscovery.js +117 -0
  51. package/lib/sourceTree.js +185 -0
  52. package/lib/sourceUpgrade.js +596 -0
  53. package/lib/sourceUpgradeAdapter.js +41 -0
  54. package/lib/sourceUpgradePolicy.js +170 -0
  55. package/lib/sourceUpgradeTransaction.js +202 -0
  56. package/lib/sourceValidation.js +120 -0
  57. package/lib/sourceWorkspace.js +37 -0
  58. package/lib/specGate.js +235 -0
  59. package/lib/tuiActions.js +169 -0
  60. package/lib/tuiInstall.js +133 -0
  61. package/lib/tuiLayout.js +94 -0
  62. package/lib/version.js +17 -0
  63. package/lib/webManager.js +370 -0
  64. package/package.json +74 -0
  65. package/public/agentsUi.js +138 -0
  66. package/public/app.js +1431 -0
  67. package/public/claude.css +2 -0
  68. package/public/claudeUi.js +119 -0
  69. package/public/emptyState.js +12 -0
  70. package/public/enabled.css +303 -0
  71. package/public/favicon.svg +5 -0
  72. package/public/index.html +199 -0
  73. package/public/skillcaddy_CN.png +0 -0
  74. package/public/skillcaddy_EN.png +0 -0
  75. package/public/styles.css +1611 -0
  76. package/scripts/cli.js +647 -0
  77. package/scripts/cron-sync-update-git.sh +128 -0
  78. package/scripts/pull-github.sh +22 -0
  79. package/scripts/source.js +638 -0
  80. package/scripts/tui.js +1058 -0
  81. package/server.js +196 -0
  82. package/skills/skillcaddy-manager/SKILL.md +93 -0
  83. package/skills/skillcaddy-manager/agents/openai.yaml +7 -0
  84. package/skills/skillcaddy-manager/references/METADATA.md +75 -0
  85. package/skills/skillcaddy-manager/references/OPERATIONS.md +315 -0
  86. package/skills/skillcaddy-manager/references/RECOMMENDATION_GUIDE.md +51 -0
  87. package/skills/skillcaddy-manager/references/featured-skills.json +564 -0
  88. package/skills/skillcaddy-manager/references/skill-platforms.json +48 -0
  89. package/skills/skillcaddy-manager/scripts/check-conflicts.cjs +117 -0
  90. package/skills/skillcaddy-manager/scripts/check-global-skills.cjs +169 -0
  91. package/skills/skillcaddy-manager/scripts/translate-skill-notes.cjs +297 -0
  92. package/skills/skillcaddy-manager/scripts/version-manager.cjs +146 -0
  93. package/skills/skillcaddy-manager/scripts/view-recommendations.cjs +279 -0
  94. package/skills/skillcaddy-manager/skillcaddy.json +9 -0
  95. package/skills/spec-pipeline/spec-direction-gate/SKILL.md +119 -0
  96. package/skills/spec-pipeline/spec-direction-gate/check-approval.py +110 -0
  97. package/skills/spec-pipeline/spec-draft-intake/SKILL.md +116 -0
  98. package/skills/spec-pipeline/spec-role-contract/SKILL.md +113 -0
  99. package/skills/spec-pipeline/spec-role-contract/check-roles.py +108 -0
@@ -0,0 +1,62 @@
1
+ const GITHUB_HOSTNAMES = new Set(['github.com', 'www.github.com']);
2
+
3
+ export function isRemoteSkillFilePathInput(input) {
4
+ const url = parseUrl(input);
5
+ return Boolean(
6
+ url &&
7
+ ['http:', 'https:'].includes(url.protocol) &&
8
+ url.pathname.endsWith('/SKILL.md')
9
+ );
10
+ }
11
+
12
+ export function isGitHubBlobSkillFileInput(input) {
13
+ const url = parseUrl(input);
14
+ return Boolean(
15
+ url &&
16
+ GITHUB_HOSTNAMES.has(url.hostname.toLowerCase()) &&
17
+ url.pathname.endsWith('/SKILL.md') &&
18
+ url.pathname.split('/').includes('blob')
19
+ );
20
+ }
21
+
22
+ export function validateRemoteSkillFileUrl(input, options = {}) {
23
+ const url = parseUrl(input);
24
+ if (!url || !['http:', 'https:'].includes(url.protocol)) {
25
+ throw new Error('Remote file source input must be a valid HTTP(S) URL');
26
+ }
27
+ if (url.username || url.password) {
28
+ throw new Error('Remote file source URL must not contain credentials');
29
+ }
30
+ if (url.search) {
31
+ throw new Error('Remote file source URL must not contain a query');
32
+ }
33
+ if (!url.pathname.endsWith('/SKILL.md')) {
34
+ throw new Error('Remote file source URL path must end with /SKILL.md');
35
+ }
36
+ if (isGitHubBlobSkillFileInput(url)) {
37
+ throw new Error('Remote file source requires a direct SKILL.md URL');
38
+ }
39
+ if (!options.allowFragment && url.hash) {
40
+ throw new Error('Remote file source URL must not contain a fragment');
41
+ }
42
+
43
+ const sanitized = new URL(url);
44
+ sanitized.username = '';
45
+ sanitized.password = '';
46
+ sanitized.search = '';
47
+ sanitized.hash = '';
48
+ return {
49
+ url,
50
+ display: sanitized.toString(),
51
+ protocol: url.protocol.slice(0, -1)
52
+ };
53
+ }
54
+
55
+ function parseUrl(input) {
56
+ if (typeof input !== 'string' && !(input instanceof URL)) return null;
57
+ try {
58
+ return new URL(input.toString().trim());
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
@@ -0,0 +1,55 @@
1
+ import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { SourceAcquisitionError } from './sourceAcquisitionError.js';
5
+ import { downloadRemoteSkillFile } from './sourceHttp.js';
6
+ import { checksumDirectory } from './sourceTree.js';
7
+ import { validateStagedSource } from './sourceValidation.js';
8
+
9
+ export async function inspectRemoteSkillFile(context, parsed, prefix) {
10
+ const stagingRoot = await mkdtemp(path.join(tmpdir(), prefix));
11
+ try {
12
+ return await stageRemoteSkillFile(context, parsed, stagingRoot);
13
+ } finally {
14
+ await rm(stagingRoot, { recursive: true, force: true });
15
+ }
16
+ }
17
+
18
+ export async function stageRemoteSkillFile(context, parsed, stagingRoot) {
19
+ const contentRoot = path.join(stagingRoot, 'content');
20
+ await mkdir(contentRoot);
21
+ const skillFile = path.join(contentRoot, 'SKILL.md');
22
+ await downloadRemoteSkillFile(parsed.input, skillFile, {
23
+ limits: context.httpLimits,
24
+ lookup: context.httpLookup
25
+ });
26
+ await validateRemoteSkillFileBytes(skillFile);
27
+ const validated = await validateStagedSource(contentRoot);
28
+ return {
29
+ contentRoot,
30
+ integrity: {
31
+ algorithm: 'sha256',
32
+ value: await checksumDirectory(contentRoot)
33
+ },
34
+ skills: validated.skills,
35
+ warnings: validated.warnings
36
+ };
37
+ }
38
+
39
+ async function validateRemoteSkillFileBytes(skillFile) {
40
+ const content = await readFile(skillFile);
41
+ if (content.includes(0)) {
42
+ throw new SourceAcquisitionError(
43
+ 'source-validation',
44
+ 'Remote SKILL.md must not contain NUL bytes'
45
+ );
46
+ }
47
+ try {
48
+ new TextDecoder('utf-8', { fatal: true }).decode(content);
49
+ } catch {
50
+ throw new SourceAcquisitionError(
51
+ 'source-validation',
52
+ 'Remote SKILL.md must contain valid UTF-8'
53
+ );
54
+ }
55
+ }
@@ -0,0 +1,117 @@
1
+ import { lstat, readdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ const EXCLUDED_SKILL_DIRECTORIES = new Set(['docs', 'references', 'tests']);
5
+ const MAX_SKILL_DISCOVERY_DEPTH = 4;
6
+
7
+ export async function discoverSourceSkillDirectories(sourceRoot) {
8
+ if (await pathEntryExists(path.join(sourceRoot, 'SKILL.md'))) return [sourceRoot];
9
+
10
+ const skillsRoot = path.join(sourceRoot, 'skills');
11
+ if (await directoryExists(skillsRoot)) {
12
+ const standardSkills = await findNestedSkillDirectories(
13
+ skillsRoot,
14
+ MAX_SKILL_DISCOVERY_DEPTH
15
+ );
16
+ if (standardSkills.length > 0) return standardSkills;
17
+ }
18
+
19
+ // Agent Skills 标准容器: `.agents/skills/`
20
+ // 这是 Anthropic 等多 agent 工具约定的官方位置;当仓库既没有 `skills/`
21
+ // 也没有 plugin-style `<name>/skills/` 集合,但仍然遵循该约定时,识别
22
+ // 这里的 skill。该位置优先级低于 `skills/`,仅作为第二标准容器。
23
+ const agentSkillsRoot = path.join(sourceRoot, '.agents', 'skills');
24
+ if (await directoryExists(agentSkillsRoot)) {
25
+ const agentSkills = await findNestedSkillDirectories(
26
+ agentSkillsRoot,
27
+ MAX_SKILL_DISCOVERY_DEPTH
28
+ );
29
+ if (agentSkills.length > 0) return agentSkills;
30
+ }
31
+
32
+ const collectionSkills = await findCollectionSkillDirectories(sourceRoot);
33
+ if (collectionSkills.length > 0) return collectionSkills;
34
+
35
+ const fallbackSkills = [];
36
+ const singularSkillRoot = path.join(sourceRoot, 'skill');
37
+ if (await directoryExists(singularSkillRoot)) {
38
+ fallbackSkills.push(...await findNestedSkillDirectories(
39
+ singularSkillRoot,
40
+ MAX_SKILL_DISCOVERY_DEPTH
41
+ ));
42
+ }
43
+ fallbackSkills.push(...await findDirectSkillDirectories(sourceRoot));
44
+
45
+ return [...new Set(fallbackSkills)].sort((left, right) => left.localeCompare(right));
46
+ }
47
+
48
+ async function findCollectionSkillDirectories(directory) {
49
+ const entries = await readdir(directory, { withFileTypes: true });
50
+ const skills = [];
51
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
52
+ if (
53
+ !entry.isDirectory() ||
54
+ entry.name.startsWith('.') ||
55
+ EXCLUDED_SKILL_DIRECTORIES.has(entry.name) ||
56
+ ['skill', 'skills'].includes(entry.name)
57
+ ) continue;
58
+ const collectionSkillsRoot = path.join(directory, entry.name, 'skills');
59
+ if (!await directoryExists(collectionSkillsRoot)) continue;
60
+ skills.push(...await findNestedSkillDirectories(
61
+ collectionSkillsRoot,
62
+ MAX_SKILL_DISCOVERY_DEPTH
63
+ ));
64
+ }
65
+ return skills;
66
+ }
67
+
68
+ async function findNestedSkillDirectories(directory, depth) {
69
+ if (depth < 0) return [];
70
+ if (await pathEntryExists(path.join(directory, 'SKILL.md'))) return [directory];
71
+
72
+ const entries = await readdir(directory, { withFileTypes: true });
73
+ const nested = [];
74
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
75
+ if (
76
+ !entry.isDirectory() ||
77
+ entry.name.startsWith('.') ||
78
+ EXCLUDED_SKILL_DIRECTORIES.has(entry.name)
79
+ ) continue;
80
+ nested.push(...await findNestedSkillDirectories(path.join(directory, entry.name), depth - 1));
81
+ }
82
+ return nested;
83
+ }
84
+
85
+ async function findDirectSkillDirectories(directory) {
86
+ const entries = await readdir(directory, { withFileTypes: true });
87
+ const skills = [];
88
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
89
+ if (
90
+ !entry.isDirectory() ||
91
+ entry.name.startsWith('.') ||
92
+ EXCLUDED_SKILL_DIRECTORIES.has(entry.name)
93
+ ) continue;
94
+ const candidate = path.join(directory, entry.name);
95
+ if (await pathEntryExists(path.join(candidate, 'SKILL.md'))) skills.push(candidate);
96
+ }
97
+ return skills;
98
+ }
99
+
100
+ async function pathEntryExists(targetPath) {
101
+ try {
102
+ await lstat(targetPath);
103
+ return true;
104
+ } catch (error) {
105
+ if (error.code === 'ENOENT') return false;
106
+ throw error;
107
+ }
108
+ }
109
+
110
+ async function directoryExists(targetPath) {
111
+ try {
112
+ return (await lstat(targetPath)).isDirectory();
113
+ } catch (error) {
114
+ if (error.code === 'ENOENT') return false;
115
+ throw error;
116
+ }
117
+ }
@@ -0,0 +1,185 @@
1
+ import { createHash } from 'node:crypto';
2
+ import {
3
+ lstat,
4
+ mkdir,
5
+ readFile,
6
+ readdir,
7
+ readlink,
8
+ realpath
9
+ } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { SourceMigrationIssue } from './sourceMigrationIssue.js';
12
+ import { isPathInsideOrEqual } from './sourcePath.js';
13
+ import { SOURCE_FOLDERS } from './sourcePolicy.js';
14
+
15
+ export async function findSourceEntries(rootDir) {
16
+ const entries = [];
17
+ for (const bucket of SOURCE_FOLDERS) {
18
+ const bucketPath = path.join(rootDir, bucket);
19
+ for (const entry of await readSourceDirectory(bucketPath)) {
20
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
21
+ entries.push({
22
+ bucket,
23
+ installPath: `${bucket}/${entry.name}`,
24
+ sourcePath: path.join(bucketPath, entry.name),
25
+ unsafe: entry.isSymbolicLink()
26
+ });
27
+ }
28
+ }
29
+ }
30
+ return entries;
31
+ }
32
+
33
+ export async function validateSourceTree(rootDir, sourcePath) {
34
+ const [resolvedRoot, resolvedSource] = await Promise.all([
35
+ realpath(path.resolve(rootDir)),
36
+ realpath(sourcePath)
37
+ ]);
38
+ if (!isPathInsideOrEqual(resolvedRoot, resolvedSource)) {
39
+ throw new SourceMigrationIssue('unsafe-path', 'Source path resolves outside the central-library root');
40
+ }
41
+
42
+ async function walk(directory, relativeDirectory = '') {
43
+ const entries = await readdir(directory, { withFileTypes: true });
44
+ for (const entry of entries) {
45
+ const entryPath = path.join(directory, entry.name);
46
+ const relativePath = path.posix.join(relativeDirectory, entry.name);
47
+ if (entry.name === '.git') {
48
+ if (entry.isSymbolicLink()) {
49
+ throw new SourceMigrationIssue('unsafe-path', `Git metadata is a symbolic link: ${relativePath}`);
50
+ }
51
+ if (relativeDirectory) {
52
+ throw new SourceMigrationIssue('nested-repository', `Nested Git repository: ${relativePath}`);
53
+ }
54
+ continue;
55
+ }
56
+ if (entry.isDirectory()) {
57
+ await walk(entryPath, relativePath);
58
+ } else if (entry.isSymbolicLink()) {
59
+ await validateSymlink(resolvedSource, entryPath, relativePath);
60
+ } else if (!entry.isFile()) {
61
+ const stat = await lstat(entryPath);
62
+ throw new SourceMigrationIssue('unsafe-path', `Unsupported entry mode ${stat.mode}: ${relativePath}`);
63
+ }
64
+ }
65
+ }
66
+
67
+ await walk(sourcePath);
68
+ }
69
+
70
+ export async function checksumDirectory(directory) {
71
+ return checksumDirectoryWithOptions(directory, false);
72
+ }
73
+
74
+ /**
75
+ * Compute the image-side checksum after applying the NFC path policy from
76
+ * ADR-0011/#35. The ordinary checksum remains unchanged for existing source
77
+ * acquisition and registry contracts.
78
+ */
79
+ export async function checksumDirectoryNormalized(directory) {
80
+ return checksumDirectoryWithOptions(directory, true);
81
+ }
82
+
83
+ async function checksumDirectoryWithOptions(directory, normalizePaths) {
84
+ const hash = createHash('sha256');
85
+
86
+ async function walk(currentPath) {
87
+ const entries = await readdir(currentPath, { withFileTypes: true });
88
+ for (const entry of entries.sort((left, right) => {
89
+ const leftName = normalizePaths ? left.name.normalize('NFC') : left.name;
90
+ const rightName = normalizePaths ? right.name.normalize('NFC') : right.name;
91
+ return leftName.localeCompare(rightName) || left.name.localeCompare(right.name);
92
+ })) {
93
+ const entryPath = path.join(currentPath, entry.name);
94
+ const relativePath = path.relative(directory, entryPath)
95
+ .split(path.sep)
96
+ .join('/');
97
+ const hashedPath = normalizePaths ? relativePath.normalize('NFC') : relativePath;
98
+ if (entry.isDirectory()) {
99
+ hash.update(`directory\0${hashedPath}\0`);
100
+ await walk(entryPath);
101
+ } else if (entry.isFile()) {
102
+ hash.update(`file\0${hashedPath}\0`);
103
+ hash.update(await readFile(entryPath));
104
+ hash.update('\0');
105
+ } else if (entry.isSymbolicLink()) {
106
+ const target = await readlink(entryPath);
107
+ hash.update(`symlink\0${hashedPath}\0${normalizePaths ? target.normalize('NFC') : target}\0`);
108
+ } else {
109
+ throw new Error(`Unsupported source entry: ${hashedPath}`);
110
+ }
111
+ }
112
+ }
113
+
114
+ await walk(directory);
115
+ return hash.digest('hex');
116
+ }
117
+
118
+ export async function readSourceDirectory(directory) {
119
+ try {
120
+ return await readdir(directory, { withFileTypes: true });
121
+ } catch (error) {
122
+ if (error.code === 'ENOENT') return [];
123
+ throw error;
124
+ }
125
+ }
126
+
127
+ export async function sourcePathExists(targetPath) {
128
+ try {
129
+ await lstat(targetPath);
130
+ return true;
131
+ } catch (error) {
132
+ if (error.code === 'ENOENT') return false;
133
+ throw error;
134
+ }
135
+ }
136
+
137
+ export async function sourceInstallPathAvailable(rootDir, records, installPath) {
138
+ return !records.some((record) => record.installPath === installPath) &&
139
+ !await sourcePathExists(path.join(rootDir, ...installPath.split('/')));
140
+ }
141
+
142
+ export async function ensureSourceDirectory(rootDir, ...segments) {
143
+ let current = await realpath(path.resolve(rootDir));
144
+ const resolvedRoot = current;
145
+
146
+ for (const segment of segments) {
147
+ if (!segment || segment.includes('/') || segment.includes('\\')) {
148
+ throw new Error('Invalid managed source directory segment');
149
+ }
150
+ const next = path.join(current, segment);
151
+ let stat;
152
+ try {
153
+ stat = await lstat(next);
154
+ } catch (error) {
155
+ if (error.code !== 'ENOENT') throw error;
156
+ await mkdir(next);
157
+ stat = await lstat(next);
158
+ }
159
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
160
+ throw new Error(`Managed source directory is not a safe directory: ${segment}`);
161
+ }
162
+ current = await realpath(next);
163
+ if (!isPathInsideOrEqual(resolvedRoot, current)) {
164
+ throw new Error(`Managed source directory resolves outside the central-library root: ${segment}`);
165
+ }
166
+ }
167
+ return current;
168
+ }
169
+
170
+ async function validateSymlink(resolvedSource, entryPath, relativePath) {
171
+ const target = await readlink(entryPath);
172
+ if (path.isAbsolute(target)) {
173
+ throw new SourceMigrationIssue('unsafe-path', `Absolute symlink: ${relativePath}`);
174
+ }
175
+
176
+ let resolvedTarget;
177
+ try {
178
+ resolvedTarget = await realpath(entryPath);
179
+ } catch {
180
+ throw new SourceMigrationIssue('unsafe-path', `Broken symlink: ${relativePath}`);
181
+ }
182
+ if (!isPathInsideOrEqual(resolvedSource, resolvedTarget)) {
183
+ throw new SourceMigrationIssue('unsafe-path', `Escaping symlink: ${relativePath}`);
184
+ }
185
+ }