prjct-cli 0.39.0 → 0.40.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.
- package/CHANGELOG.md +38 -0
- package/core/agentic/orchestrator-executor.ts +23 -6
- package/core/services/skill-installer.ts +431 -0
- package/core/services/skill-lock.ts +132 -0
- package/core/services/skill-service.ts +34 -24
- package/core/types/services.ts +6 -1
- package/package.json +1 -1
- package/templates/commands/skill.md +174 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.40.0] - 2026-01-28
|
|
4
|
+
|
|
5
|
+
### Feature: Enhanced Skill System
|
|
6
|
+
|
|
7
|
+
Inspired by vercel-labs/skills, prjct-cli now supports remote skill installation, version tracking via lock file, and full ecosystem-standard SKILL.md subdirectory format across all skill directories.
|
|
8
|
+
|
|
9
|
+
**New: Remote Skill Installation (`p. skill add`)**
|
|
10
|
+
- Install skills from GitHub repos: `p. skill add owner/repo`
|
|
11
|
+
- Install specific skill: `p. skill add owner/repo@skill-name`
|
|
12
|
+
- Install from local directory: `p. skill add ./path`
|
|
13
|
+
- Automatic source metadata injection (`_prjct` frontmatter block)
|
|
14
|
+
|
|
15
|
+
**New: Skill Lock File**
|
|
16
|
+
- Tracks installed skills at `~/.prjct-cli/skills/.skill-lock.json`
|
|
17
|
+
- Records source URL, commit SHA, install timestamp
|
|
18
|
+
- Enables update detection via `p. skill check`
|
|
19
|
+
|
|
20
|
+
**New: Additional Subcommands**
|
|
21
|
+
- `p. skill add <source>` — Install from GitHub or local path
|
|
22
|
+
- `p. skill remove <name>` — Uninstall a skill
|
|
23
|
+
- `p. skill init <name>` — Scaffold a new skill template
|
|
24
|
+
- `p. skill check` — Detect available updates
|
|
25
|
+
|
|
26
|
+
**Improved: Full SKILL.md Subdirectory Support**
|
|
27
|
+
- All skill directories now support both `{name}.md` (flat) and `{name}/SKILL.md` (subdirectory) formats
|
|
28
|
+
- Previously only provider dirs (`~/.claude/skills/`) checked subdirectories
|
|
29
|
+
- Orchestrator executor now checks both patterns when loading agent skills
|
|
30
|
+
|
|
31
|
+
**New Files:**
|
|
32
|
+
- `core/services/skill-installer.ts` — Remote installation service
|
|
33
|
+
- `core/services/skill-lock.ts` — Lock file management
|
|
34
|
+
|
|
35
|
+
**Modified:**
|
|
36
|
+
- `core/types/services.ts` — Extended `SkillMetadata` with `sourceUrl`, `sourceType`, `installedAt`, `sha`; added `'remote'` source type
|
|
37
|
+
- `core/services/skill-service.ts` — Unified SKILL.md discovery in all dirs
|
|
38
|
+
- `core/agentic/orchestrator-executor.ts` — Both path patterns for skill loading
|
|
39
|
+
- `templates/commands/skill.md` — New subcommands documentation
|
|
40
|
+
|
|
3
41
|
## [0.39.0] - 2026-01-24
|
|
4
42
|
|
|
5
43
|
### Feature: Windsurf IDE Support (PRJ-66)
|
|
@@ -344,18 +344,35 @@ export class OrchestratorExecutor {
|
|
|
344
344
|
// Skip if already loaded
|
|
345
345
|
if (loadedSkillNames.has(skillName)) continue
|
|
346
346
|
|
|
347
|
-
|
|
347
|
+
// Check both patterns: flat file and subdirectory (ecosystem standard)
|
|
348
|
+
const flatPath = path.join(skillsDir, `${skillName}.md`)
|
|
349
|
+
const subdirPath = path.join(skillsDir, skillName, 'SKILL.md')
|
|
350
|
+
|
|
351
|
+
let content: string | null = null
|
|
352
|
+
let resolvedPath = flatPath
|
|
353
|
+
|
|
354
|
+
// Prefer subdirectory format (ecosystem standard)
|
|
348
355
|
try {
|
|
349
|
-
|
|
356
|
+
content = await fs.readFile(subdirPath, 'utf-8')
|
|
357
|
+
resolvedPath = subdirPath
|
|
358
|
+
} catch {
|
|
359
|
+
// Fall back to flat file
|
|
360
|
+
try {
|
|
361
|
+
content = await fs.readFile(flatPath, 'utf-8')
|
|
362
|
+
resolvedPath = flatPath
|
|
363
|
+
} catch {
|
|
364
|
+
// Skill not found - not an error, just skip
|
|
365
|
+
console.warn(`Skill not found: ${skillName}`)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (content) {
|
|
350
370
|
skills.push({
|
|
351
371
|
name: skillName,
|
|
352
372
|
content,
|
|
353
|
-
filePath:
|
|
373
|
+
filePath: resolvedPath,
|
|
354
374
|
})
|
|
355
375
|
loadedSkillNames.add(skillName)
|
|
356
|
-
} catch {
|
|
357
|
-
// Skill not found - not an error, just skip
|
|
358
|
-
console.warn(`Skill not found: ${skillName}`)
|
|
359
376
|
}
|
|
360
377
|
}
|
|
361
378
|
}
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Installer Service
|
|
3
|
+
*
|
|
4
|
+
* Installs skills from remote sources (GitHub repos, local paths).
|
|
5
|
+
* Follows the ecosystem standard of {name}/SKILL.md subdirectory format.
|
|
6
|
+
*
|
|
7
|
+
* Supported sources:
|
|
8
|
+
* - owner/repo — clone from GitHub, discover all SKILL.md files
|
|
9
|
+
* - owner/repo@skill-name — install specific skill from repo
|
|
10
|
+
* - ./local-path — install from local directory
|
|
11
|
+
*
|
|
12
|
+
* @version 1.0.0
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'fs/promises'
|
|
16
|
+
import path from 'path'
|
|
17
|
+
import os from 'os'
|
|
18
|
+
import { promisify } from 'util'
|
|
19
|
+
import { exec as execCallback } from 'child_process'
|
|
20
|
+
import { glob } from 'glob'
|
|
21
|
+
|
|
22
|
+
import { skillLock } from './skill-lock'
|
|
23
|
+
import type { SkillLockEntry } from './skill-lock'
|
|
24
|
+
|
|
25
|
+
const exec = promisify(execCallback)
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Types
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
export interface ParsedSource {
|
|
32
|
+
type: 'github' | 'local'
|
|
33
|
+
owner?: string
|
|
34
|
+
repo?: string
|
|
35
|
+
skillName?: string
|
|
36
|
+
localPath?: string
|
|
37
|
+
url: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface InstalledSkill {
|
|
41
|
+
name: string
|
|
42
|
+
filePath: string
|
|
43
|
+
source: ParsedSource
|
|
44
|
+
sha?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface InstallResult {
|
|
48
|
+
installed: InstalledSkill[]
|
|
49
|
+
skipped: string[]
|
|
50
|
+
errors: string[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// Source Parsing
|
|
55
|
+
// =============================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse a source string into a structured source object
|
|
59
|
+
*
|
|
60
|
+
* Formats:
|
|
61
|
+
* - "owner/repo" → GitHub repo, install all skills
|
|
62
|
+
* - "owner/repo@skill-name" → GitHub repo, specific skill
|
|
63
|
+
* - "./path" or "/path" → Local directory
|
|
64
|
+
*/
|
|
65
|
+
export function parseSource(source: string): ParsedSource {
|
|
66
|
+
// Local path
|
|
67
|
+
if (source.startsWith('./') || source.startsWith('/') || source.startsWith('~')) {
|
|
68
|
+
const resolvedPath = source.startsWith('~')
|
|
69
|
+
? path.join(os.homedir(), source.slice(1))
|
|
70
|
+
: path.resolve(source)
|
|
71
|
+
return {
|
|
72
|
+
type: 'local',
|
|
73
|
+
localPath: resolvedPath,
|
|
74
|
+
url: resolvedPath,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// GitHub: owner/repo@skill-name
|
|
79
|
+
const atIndex = source.indexOf('@')
|
|
80
|
+
if (atIndex > 0) {
|
|
81
|
+
const repoPath = source.slice(0, atIndex)
|
|
82
|
+
const skillName = source.slice(atIndex + 1)
|
|
83
|
+
const [owner, repo] = repoPath.split('/')
|
|
84
|
+
if (owner && repo) {
|
|
85
|
+
return {
|
|
86
|
+
type: 'github',
|
|
87
|
+
owner,
|
|
88
|
+
repo,
|
|
89
|
+
skillName,
|
|
90
|
+
url: `https://github.com/${owner}/${repo}`,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// GitHub: owner/repo
|
|
96
|
+
const parts = source.split('/')
|
|
97
|
+
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
98
|
+
return {
|
|
99
|
+
type: 'github',
|
|
100
|
+
owner: parts[0],
|
|
101
|
+
repo: parts[1],
|
|
102
|
+
url: `https://github.com/${parts[0]}/${parts[1]}`,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error(`Invalid source format: "${source}". Expected "owner/repo", "owner/repo@skill-name", or "./local-path"`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// =============================================================================
|
|
110
|
+
// Skill Discovery
|
|
111
|
+
// =============================================================================
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Discover skills in a directory by scanning for SKILL.md files
|
|
115
|
+
*/
|
|
116
|
+
async function discoverSkills(dir: string): Promise<Array<{ name: string; filePath: string }>> {
|
|
117
|
+
const skills: Array<{ name: string; filePath: string }> = []
|
|
118
|
+
|
|
119
|
+
// Pattern 1: {dir}/SKILL.md (root-level skill)
|
|
120
|
+
try {
|
|
121
|
+
const rootSkill = path.join(dir, 'SKILL.md')
|
|
122
|
+
await fs.access(rootSkill)
|
|
123
|
+
const dirName = path.basename(dir)
|
|
124
|
+
skills.push({ name: dirName, filePath: rootSkill })
|
|
125
|
+
} catch {
|
|
126
|
+
// No root SKILL.md
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Pattern 2: {dir}/*/SKILL.md (subdirectory skills)
|
|
130
|
+
const subdirSkills = await glob('*/SKILL.md', { cwd: dir, absolute: true })
|
|
131
|
+
for (const filePath of subdirSkills) {
|
|
132
|
+
const name = path.basename(path.dirname(filePath))
|
|
133
|
+
// Avoid duplicate if already found as root
|
|
134
|
+
if (!skills.some(s => s.name === name)) {
|
|
135
|
+
skills.push({ name, filePath })
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Pattern 3: {dir}/skills/*/SKILL.md (nested skills directory)
|
|
140
|
+
const nestedSkills = await glob('skills/*/SKILL.md', { cwd: dir, absolute: true })
|
|
141
|
+
for (const filePath of nestedSkills) {
|
|
142
|
+
const name = path.basename(path.dirname(filePath))
|
|
143
|
+
if (!skills.some(s => s.name === name)) {
|
|
144
|
+
skills.push({ name, filePath })
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return skills
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// =============================================================================
|
|
152
|
+
// Frontmatter Injection
|
|
153
|
+
// =============================================================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Add _prjct source tracking metadata to a skill's frontmatter
|
|
157
|
+
*/
|
|
158
|
+
function injectSourceMetadata(
|
|
159
|
+
content: string,
|
|
160
|
+
source: ParsedSource,
|
|
161
|
+
sha?: string
|
|
162
|
+
): string {
|
|
163
|
+
const now = new Date().toISOString()
|
|
164
|
+
const prjctBlock = [
|
|
165
|
+
`_prjct:`,
|
|
166
|
+
` sourceUrl: ${source.url}`,
|
|
167
|
+
` sourceType: ${source.type}`,
|
|
168
|
+
` installedAt: ${now}`,
|
|
169
|
+
]
|
|
170
|
+
if (sha) {
|
|
171
|
+
prjctBlock.push(` sha: ${sha}`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check if file has existing frontmatter
|
|
175
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---/
|
|
176
|
+
const match = content.match(frontmatterRegex)
|
|
177
|
+
|
|
178
|
+
if (match) {
|
|
179
|
+
// Remove existing _prjct block if present
|
|
180
|
+
let frontmatter = match[1]
|
|
181
|
+
frontmatter = frontmatter.replace(/\n?_prjct:[\s\S]*?(?=\n[a-zA-Z]|\n---|\s*$)/g, '')
|
|
182
|
+
|
|
183
|
+
// Append _prjct block
|
|
184
|
+
const updatedFrontmatter = frontmatter.trimEnd() + '\n' + prjctBlock.join('\n')
|
|
185
|
+
return content.replace(frontmatterRegex, `---\n${updatedFrontmatter}\n---`)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// No frontmatter — add one
|
|
189
|
+
return `---\n${prjctBlock.join('\n')}\n---\n\n${content}`
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// =============================================================================
|
|
193
|
+
// Installation
|
|
194
|
+
// =============================================================================
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get the default install directory for skills
|
|
198
|
+
*/
|
|
199
|
+
function getInstallDir(): string {
|
|
200
|
+
return path.join(os.homedir(), '.claude', 'skills')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Install a single skill file to the target directory
|
|
205
|
+
*/
|
|
206
|
+
async function installSkillFile(
|
|
207
|
+
sourcePath: string,
|
|
208
|
+
name: string,
|
|
209
|
+
source: ParsedSource,
|
|
210
|
+
sha?: string
|
|
211
|
+
): Promise<InstalledSkill> {
|
|
212
|
+
const installDir = getInstallDir()
|
|
213
|
+
const targetDir = path.join(installDir, name)
|
|
214
|
+
const targetPath = path.join(targetDir, 'SKILL.md')
|
|
215
|
+
|
|
216
|
+
// Read source content
|
|
217
|
+
const content = await fs.readFile(sourcePath, 'utf-8')
|
|
218
|
+
|
|
219
|
+
// Inject source metadata
|
|
220
|
+
const enrichedContent = injectSourceMetadata(content, source, sha)
|
|
221
|
+
|
|
222
|
+
// Write to target (ecosystem standard: {name}/SKILL.md)
|
|
223
|
+
await fs.mkdir(targetDir, { recursive: true })
|
|
224
|
+
await fs.writeFile(targetPath, enrichedContent, 'utf-8')
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
name,
|
|
228
|
+
filePath: targetPath,
|
|
229
|
+
source,
|
|
230
|
+
sha,
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Install skills from a GitHub repository
|
|
236
|
+
*/
|
|
237
|
+
async function installFromGitHub(source: ParsedSource): Promise<InstallResult> {
|
|
238
|
+
const result: InstallResult = { installed: [], skipped: [], errors: [] }
|
|
239
|
+
|
|
240
|
+
// Create temp directory
|
|
241
|
+
const tmpDir = path.join(os.tmpdir(), `prjct-skill-${Date.now()}`)
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
// Clone with depth 1 for speed
|
|
245
|
+
const cloneUrl = `https://github.com/${source.owner}/${source.repo}.git`
|
|
246
|
+
await exec(`git clone --depth 1 ${cloneUrl} ${tmpDir}`, { timeout: 60_000 })
|
|
247
|
+
|
|
248
|
+
// Get the commit SHA
|
|
249
|
+
let sha: string | undefined
|
|
250
|
+
try {
|
|
251
|
+
const { stdout } = await exec('git rev-parse HEAD', { cwd: tmpDir, timeout: 5_000 })
|
|
252
|
+
sha = stdout.trim()
|
|
253
|
+
} catch {
|
|
254
|
+
// Non-critical
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Discover skills in the cloned repo
|
|
258
|
+
const discoveredSkills = await discoverSkills(tmpDir)
|
|
259
|
+
|
|
260
|
+
if (discoveredSkills.length === 0) {
|
|
261
|
+
result.errors.push(`No SKILL.md files found in ${source.owner}/${source.repo}`)
|
|
262
|
+
return result
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Filter to specific skill if requested
|
|
266
|
+
const skillsToInstall = source.skillName
|
|
267
|
+
? discoveredSkills.filter(s => s.name === source.skillName)
|
|
268
|
+
: discoveredSkills
|
|
269
|
+
|
|
270
|
+
if (source.skillName && skillsToInstall.length === 0) {
|
|
271
|
+
result.errors.push(`Skill "${source.skillName}" not found in ${source.owner}/${source.repo}`)
|
|
272
|
+
return result
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Install each skill
|
|
276
|
+
for (const skill of skillsToInstall) {
|
|
277
|
+
try {
|
|
278
|
+
const installed = await installSkillFile(skill.filePath, skill.name, source, sha)
|
|
279
|
+
|
|
280
|
+
// Update lock file
|
|
281
|
+
const lockEntry: SkillLockEntry = {
|
|
282
|
+
name: skill.name,
|
|
283
|
+
source: {
|
|
284
|
+
type: 'github',
|
|
285
|
+
url: `${source.owner}/${source.repo}`,
|
|
286
|
+
sha,
|
|
287
|
+
},
|
|
288
|
+
installedAt: new Date().toISOString(),
|
|
289
|
+
filePath: installed.filePath,
|
|
290
|
+
}
|
|
291
|
+
await skillLock.addEntry(lockEntry)
|
|
292
|
+
|
|
293
|
+
result.installed.push(installed)
|
|
294
|
+
} catch (error) {
|
|
295
|
+
result.errors.push(`Failed to install ${skill.name}: ${(error as Error).message}`)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} finally {
|
|
299
|
+
// Clean up temp directory
|
|
300
|
+
try {
|
|
301
|
+
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
302
|
+
} catch {
|
|
303
|
+
// Best effort cleanup
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return result
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Install skills from a local directory
|
|
312
|
+
*/
|
|
313
|
+
async function installFromLocal(source: ParsedSource): Promise<InstallResult> {
|
|
314
|
+
const result: InstallResult = { installed: [], skipped: [], errors: [] }
|
|
315
|
+
const localPath = source.localPath!
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
await fs.access(localPath)
|
|
319
|
+
} catch {
|
|
320
|
+
result.errors.push(`Local path not found: ${localPath}`)
|
|
321
|
+
return result
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const stat = await fs.stat(localPath)
|
|
325
|
+
|
|
326
|
+
if (stat.isFile()) {
|
|
327
|
+
// Single SKILL.md file
|
|
328
|
+
const name = path.basename(path.dirname(localPath))
|
|
329
|
+
try {
|
|
330
|
+
const installed = await installSkillFile(localPath, name, source)
|
|
331
|
+
const lockEntry: SkillLockEntry = {
|
|
332
|
+
name,
|
|
333
|
+
source: { type: 'local', url: localPath },
|
|
334
|
+
installedAt: new Date().toISOString(),
|
|
335
|
+
filePath: installed.filePath,
|
|
336
|
+
}
|
|
337
|
+
await skillLock.addEntry(lockEntry)
|
|
338
|
+
result.installed.push(installed)
|
|
339
|
+
} catch (error) {
|
|
340
|
+
result.errors.push(`Failed to install from ${localPath}: ${(error as Error).message}`)
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
// Directory — discover skills
|
|
344
|
+
const discoveredSkills = await discoverSkills(localPath)
|
|
345
|
+
|
|
346
|
+
if (discoveredSkills.length === 0) {
|
|
347
|
+
result.errors.push(`No SKILL.md files found in ${localPath}`)
|
|
348
|
+
return result
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
for (const skill of discoveredSkills) {
|
|
352
|
+
try {
|
|
353
|
+
const installed = await installSkillFile(skill.filePath, skill.name, source)
|
|
354
|
+
const lockEntry: SkillLockEntry = {
|
|
355
|
+
name: skill.name,
|
|
356
|
+
source: { type: 'local', url: localPath },
|
|
357
|
+
installedAt: new Date().toISOString(),
|
|
358
|
+
filePath: installed.filePath,
|
|
359
|
+
}
|
|
360
|
+
await skillLock.addEntry(lockEntry)
|
|
361
|
+
result.installed.push(installed)
|
|
362
|
+
} catch (error) {
|
|
363
|
+
result.errors.push(`Failed to install ${skill.name}: ${(error as Error).message}`)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return result
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Remove an installed skill
|
|
373
|
+
*/
|
|
374
|
+
async function remove(name: string): Promise<boolean> {
|
|
375
|
+
const installDir = getInstallDir()
|
|
376
|
+
|
|
377
|
+
// Remove subdirectory format
|
|
378
|
+
const subdirPath = path.join(installDir, name)
|
|
379
|
+
try {
|
|
380
|
+
await fs.rm(subdirPath, { recursive: true, force: true })
|
|
381
|
+
} catch {
|
|
382
|
+
// May not exist in subdir format
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Remove flat file format
|
|
386
|
+
const flatPath = path.join(installDir, `${name}.md`)
|
|
387
|
+
try {
|
|
388
|
+
await fs.rm(flatPath, { force: true })
|
|
389
|
+
} catch {
|
|
390
|
+
// May not exist in flat format
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Remove from lock file
|
|
394
|
+
return skillLock.removeEntry(name)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// =============================================================================
|
|
398
|
+
// Main Install Function
|
|
399
|
+
// =============================================================================
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Install skills from a source string
|
|
403
|
+
*
|
|
404
|
+
* @param source - Source string (e.g., "owner/repo", "owner/repo@skill", "./path")
|
|
405
|
+
* @returns Installation result with installed, skipped, and error lists
|
|
406
|
+
*/
|
|
407
|
+
async function install(sourceStr: string): Promise<InstallResult> {
|
|
408
|
+
const source = parseSource(sourceStr)
|
|
409
|
+
|
|
410
|
+
switch (source.type) {
|
|
411
|
+
case 'github':
|
|
412
|
+
return installFromGitHub(source)
|
|
413
|
+
case 'local':
|
|
414
|
+
return installFromLocal(source)
|
|
415
|
+
default:
|
|
416
|
+
return {
|
|
417
|
+
installed: [],
|
|
418
|
+
skipped: [],
|
|
419
|
+
errors: [`Unsupported source type: ${source.type}`],
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export const skillInstaller = {
|
|
425
|
+
install,
|
|
426
|
+
remove,
|
|
427
|
+
parseSource,
|
|
428
|
+
getInstallDir,
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export default skillInstaller
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Lock File Service
|
|
3
|
+
*
|
|
4
|
+
* Manages a lock file that tracks remotely-installed skills for
|
|
5
|
+
* reproducibility and update detection.
|
|
6
|
+
*
|
|
7
|
+
* Lock file location: ~/.prjct-cli/skills/.skill-lock.json
|
|
8
|
+
*
|
|
9
|
+
* @version 1.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs/promises'
|
|
13
|
+
import path from 'path'
|
|
14
|
+
import os from 'os'
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
export interface SkillLockSource {
|
|
21
|
+
type: 'github' | 'local' | 'registry'
|
|
22
|
+
url: string
|
|
23
|
+
sha?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SkillLockEntry {
|
|
27
|
+
name: string
|
|
28
|
+
source: SkillLockSource
|
|
29
|
+
installedAt: string
|
|
30
|
+
filePath: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SkillLockFile {
|
|
34
|
+
version: 1
|
|
35
|
+
generatedAt: string
|
|
36
|
+
skills: Record<string, SkillLockEntry>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// Lock File Service
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
const LOCK_FILE_NAME = '.skill-lock.json'
|
|
44
|
+
|
|
45
|
+
function getLockFilePath(): string {
|
|
46
|
+
return path.join(os.homedir(), '.prjct-cli', 'skills', LOCK_FILE_NAME)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createEmptyLockFile(): SkillLockFile {
|
|
50
|
+
return {
|
|
51
|
+
version: 1,
|
|
52
|
+
generatedAt: new Date().toISOString(),
|
|
53
|
+
skills: {},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read the lock file, returning an empty lock file if it doesn't exist
|
|
59
|
+
*/
|
|
60
|
+
async function read(): Promise<SkillLockFile> {
|
|
61
|
+
try {
|
|
62
|
+
const content = await fs.readFile(getLockFilePath(), 'utf-8')
|
|
63
|
+
return JSON.parse(content) as SkillLockFile
|
|
64
|
+
} catch {
|
|
65
|
+
return createEmptyLockFile()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Write the lock file to disk
|
|
71
|
+
*/
|
|
72
|
+
async function write(lockFile: SkillLockFile): Promise<void> {
|
|
73
|
+
const lockPath = getLockFilePath()
|
|
74
|
+
await fs.mkdir(path.dirname(lockPath), { recursive: true })
|
|
75
|
+
lockFile.generatedAt = new Date().toISOString()
|
|
76
|
+
await fs.writeFile(lockPath, JSON.stringify(lockFile, null, 2), 'utf-8')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Add or update a skill entry in the lock file
|
|
81
|
+
*/
|
|
82
|
+
async function addEntry(entry: SkillLockEntry): Promise<void> {
|
|
83
|
+
const lockFile = await read()
|
|
84
|
+
lockFile.skills[entry.name] = entry
|
|
85
|
+
await write(lockFile)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Remove a skill entry from the lock file
|
|
90
|
+
*/
|
|
91
|
+
async function removeEntry(name: string): Promise<boolean> {
|
|
92
|
+
const lockFile = await read()
|
|
93
|
+
if (!(name in lockFile.skills)) return false
|
|
94
|
+
delete lockFile.skills[name]
|
|
95
|
+
await write(lockFile)
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get a single skill entry
|
|
101
|
+
*/
|
|
102
|
+
async function getEntry(name: string): Promise<SkillLockEntry | null> {
|
|
103
|
+
const lockFile = await read()
|
|
104
|
+
return lockFile.skills[name] || null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get all entries
|
|
109
|
+
*/
|
|
110
|
+
async function getAll(): Promise<Record<string, SkillLockEntry>> {
|
|
111
|
+
const lockFile = await read()
|
|
112
|
+
return lockFile.skills
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the lock file path (for display purposes)
|
|
117
|
+
*/
|
|
118
|
+
function getPath(): string {
|
|
119
|
+
return getLockFilePath()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const skillLock = {
|
|
123
|
+
read,
|
|
124
|
+
write,
|
|
125
|
+
addEntry,
|
|
126
|
+
removeEntry,
|
|
127
|
+
getEntry,
|
|
128
|
+
getAll,
|
|
129
|
+
getPath,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default skillLock
|
|
@@ -83,9 +83,9 @@ class SkillService {
|
|
|
83
83
|
/**
|
|
84
84
|
* Get all skill directories in order of priority
|
|
85
85
|
*/
|
|
86
|
-
private getSkillDirs(projectPath?: string, provider?: AIProviderName): Array<{ dir: string; source: Skill['source']
|
|
86
|
+
private getSkillDirs(projectPath?: string, provider?: AIProviderName): Array<{ dir: string; source: Skill['source'] }> {
|
|
87
87
|
const homeDir = process.env.HOME || process.env.USERPROFILE || '~'
|
|
88
|
-
const dirs: Array<{ dir: string; source: Skill['source']
|
|
88
|
+
const dirs: Array<{ dir: string; source: Skill['source'] }> = []
|
|
89
89
|
|
|
90
90
|
// Project skills (highest priority)
|
|
91
91
|
if (projectPath) {
|
|
@@ -96,11 +96,11 @@ class SkillService {
|
|
|
96
96
|
// Both use SKILL.md format, so skills are compatible
|
|
97
97
|
if (provider) {
|
|
98
98
|
const providerDir = provider === 'gemini' ? '.gemini' : '.claude'
|
|
99
|
-
dirs.push({ dir: path.join(homeDir, providerDir, 'skills'), source: 'global'
|
|
99
|
+
dirs.push({ dir: path.join(homeDir, providerDir, 'skills'), source: 'global' })
|
|
100
100
|
} else {
|
|
101
101
|
// Check both providers if no specific one is set
|
|
102
|
-
dirs.push({ dir: path.join(homeDir, '.claude', 'skills'), source: 'global'
|
|
103
|
-
dirs.push({ dir: path.join(homeDir, '.gemini', 'skills'), source: 'global'
|
|
102
|
+
dirs.push({ dir: path.join(homeDir, '.claude', 'skills'), source: 'global' })
|
|
103
|
+
dirs.push({ dir: path.join(homeDir, '.gemini', 'skills'), source: 'global' })
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// prjct global skills
|
|
@@ -123,6 +123,9 @@ class SkillService {
|
|
|
123
123
|
const id = fileToSkillId(filePath)
|
|
124
124
|
const name = (metadata.name as string) || id
|
|
125
125
|
|
|
126
|
+
// Extract _prjct source tracking metadata if present
|
|
127
|
+
const prjctMeta = metadata._prjct as Record<string, unknown> | undefined
|
|
128
|
+
|
|
126
129
|
return {
|
|
127
130
|
id,
|
|
128
131
|
name,
|
|
@@ -136,6 +139,12 @@ class SkillService {
|
|
|
136
139
|
agent: metadata.agent as string,
|
|
137
140
|
tags: metadata.tags as string[],
|
|
138
141
|
version: metadata.version as string,
|
|
142
|
+
category: metadata.category as string,
|
|
143
|
+
author: metadata.author as string,
|
|
144
|
+
sourceUrl: prjctMeta?.sourceUrl as string,
|
|
145
|
+
sourceType: prjctMeta?.sourceType as SkillMetadata['sourceType'],
|
|
146
|
+
installedAt: prjctMeta?.installedAt as string,
|
|
147
|
+
sha: prjctMeta?.sha as string,
|
|
139
148
|
},
|
|
140
149
|
}
|
|
141
150
|
} catch (_error) {
|
|
@@ -151,27 +160,27 @@ class SkillService {
|
|
|
151
160
|
this.skills.clear()
|
|
152
161
|
const dirs = this.getSkillDirs(projectPath, provider)
|
|
153
162
|
|
|
154
|
-
for (const { dir, source
|
|
163
|
+
for (const { dir, source } of dirs) {
|
|
155
164
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
// Check both patterns in ALL skill directories:
|
|
166
|
+
// 1. Flat files: {dir}/{name}.md
|
|
167
|
+
// 2. Subdirectories: {dir}/{name}/SKILL.md (ecosystem standard)
|
|
168
|
+
const flatFiles = await glob('*.md', { cwd: dir, absolute: true })
|
|
169
|
+
const subdirFiles = await glob('*/SKILL.md', { cwd: dir, absolute: true })
|
|
170
|
+
|
|
171
|
+
// Load subdirectory skills first (ecosystem standard takes priority within same dir)
|
|
172
|
+
for (const file of subdirFiles) {
|
|
173
|
+
const skill = await this.loadSkill(file, source)
|
|
174
|
+
if (skill && !this.skills.has(skill.id)) {
|
|
175
|
+
this.skills.set(skill.id, skill)
|
|
165
176
|
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
this.skills.set(skill.id, skill)
|
|
174
|
-
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Then flat files (don't override subdirectory version)
|
|
180
|
+
for (const file of flatFiles) {
|
|
181
|
+
const skill = await this.loadSkill(file, source)
|
|
182
|
+
if (skill && !this.skills.has(skill.id)) {
|
|
183
|
+
this.skills.set(skill.id, skill)
|
|
175
184
|
}
|
|
176
185
|
}
|
|
177
186
|
} catch (_error) {
|
|
@@ -256,6 +265,7 @@ class SkillService {
|
|
|
256
265
|
project: [],
|
|
257
266
|
global: [],
|
|
258
267
|
builtin: [],
|
|
268
|
+
remote: [],
|
|
259
269
|
}
|
|
260
270
|
|
|
261
271
|
for (const skill of skills) {
|
package/core/types/services.ts
CHANGED
|
@@ -28,6 +28,11 @@ export interface SkillMetadata {
|
|
|
28
28
|
version?: string
|
|
29
29
|
category?: string
|
|
30
30
|
author?: string
|
|
31
|
+
// Ecosystem compatibility fields
|
|
32
|
+
sourceUrl?: string
|
|
33
|
+
sourceType?: 'github' | 'local' | 'builtin' | 'registry'
|
|
34
|
+
installedAt?: string
|
|
35
|
+
sha?: string
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export interface Skill {
|
|
@@ -35,7 +40,7 @@ export interface Skill {
|
|
|
35
40
|
name: string
|
|
36
41
|
description: string
|
|
37
42
|
content: string
|
|
38
|
-
source: 'project' | 'global' | 'builtin'
|
|
43
|
+
source: 'project' | 'global' | 'builtin' | 'remote'
|
|
39
44
|
filePath: string
|
|
40
45
|
metadata: SkillMetadata
|
|
41
46
|
path?: string
|
package/package.json
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
---
|
|
2
|
-
allowed-tools: [Read, Glob]
|
|
2
|
+
allowed-tools: [Read, Glob, Bash]
|
|
3
3
|
description: 'List, search, and invoke skills'
|
|
4
4
|
timestamp-rule: 'None'
|
|
5
|
-
architecture: 'Skill discovery and execution'
|
|
5
|
+
architecture: 'Skill discovery, installation, and execution'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# /p:skill - Skill Management
|
|
9
9
|
|
|
10
|
-
List, search, and invoke reusable skills.
|
|
10
|
+
List, search, install, and invoke reusable skills.
|
|
11
11
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
14
14
|
```
|
|
15
|
-
/p:skill
|
|
16
|
-
/p:skill list
|
|
17
|
-
/p:skill search <query>
|
|
18
|
-
/p:skill show <id>
|
|
19
|
-
/p:skill invoke <id>
|
|
15
|
+
/p:skill # List all skills
|
|
16
|
+
/p:skill list # List all skills
|
|
17
|
+
/p:skill search <query> # Search skills
|
|
18
|
+
/p:skill show <id> # Show skill details
|
|
19
|
+
/p:skill invoke <id> # Invoke a skill
|
|
20
|
+
/p:skill add <source> # Install skill from remote source
|
|
21
|
+
/p:skill remove <name> # Remove an installed skill
|
|
22
|
+
/p:skill init <name> # Scaffold a new skill
|
|
23
|
+
/p:skill check # Check for available updates
|
|
20
24
|
```
|
|
21
25
|
|
|
22
26
|
## Flow
|
|
@@ -24,11 +28,14 @@ List, search, and invoke reusable skills.
|
|
|
24
28
|
### List Skills (`/p:skill` or `/p:skill list`)
|
|
25
29
|
|
|
26
30
|
1. Load skills from all sources:
|
|
27
|
-
- Project: `.prjct/skills/*.md`
|
|
28
|
-
-
|
|
31
|
+
- Project: `.prjct/skills/*.md` and `.prjct/skills/*/SKILL.md`
|
|
32
|
+
- Provider: `~/.claude/skills/*/SKILL.md` and `~/.claude/skills/*.md`
|
|
33
|
+
- Global: `~/.prjct-cli/skills/*.md` and `~/.prjct-cli/skills/*/SKILL.md`
|
|
29
34
|
- Built-in: `templates/skills/*.md`
|
|
30
35
|
|
|
31
|
-
2.
|
|
36
|
+
2. Check lock file at `~/.prjct-cli/skills/.skill-lock.json` for source info
|
|
37
|
+
|
|
38
|
+
3. Output grouped by source:
|
|
32
39
|
|
|
33
40
|
```
|
|
34
41
|
## Available Skills
|
|
@@ -36,13 +43,13 @@ List, search, and invoke reusable skills.
|
|
|
36
43
|
### Project Skills
|
|
37
44
|
- **custom-deploy** - Deploy to staging server
|
|
38
45
|
|
|
39
|
-
### Global Skills
|
|
46
|
+
### Global Skills (Provider)
|
|
47
|
+
- **frontend-design** - Create production-grade UIs [github: vercel-labs/skills]
|
|
40
48
|
- **my-template** - Personal code template
|
|
41
49
|
|
|
42
50
|
### Built-in Skills
|
|
43
51
|
- **code-review** - Review code for quality
|
|
44
52
|
- **refactor** - Refactor code structure
|
|
45
|
-
- **debug** - Systematic debugging
|
|
46
53
|
```
|
|
47
54
|
|
|
48
55
|
### Search Skills (`/p:skill search <query>`)
|
|
@@ -55,14 +62,17 @@ List, search, and invoke reusable skills.
|
|
|
55
62
|
|
|
56
63
|
1. Load skill by ID
|
|
57
64
|
2. Display metadata and content
|
|
65
|
+
3. If remotely installed, show source tracking info
|
|
58
66
|
|
|
59
67
|
```
|
|
60
|
-
## Skill:
|
|
68
|
+
## Skill: frontend-design
|
|
61
69
|
|
|
62
|
-
**Description:**
|
|
63
|
-
**Source:**
|
|
64
|
-
**Tags:**
|
|
65
|
-
**
|
|
70
|
+
**Description:** Create production-grade frontend interfaces
|
|
71
|
+
**Source:** global (github: vercel-labs/skills)
|
|
72
|
+
**Tags:** frontend, design, ui
|
|
73
|
+
**Version:** 1.0.0
|
|
74
|
+
**Installed:** 2026-01-28T12:00:00.000Z
|
|
75
|
+
**SHA:** abc123
|
|
66
76
|
|
|
67
77
|
### Content
|
|
68
78
|
[Full skill prompt content]
|
|
@@ -74,9 +84,122 @@ List, search, and invoke reusable skills.
|
|
|
74
84
|
2. Return skill content for execution
|
|
75
85
|
3. The skill content becomes the prompt
|
|
76
86
|
|
|
87
|
+
### Add Skill (`/p:skill add <source>`)
|
|
88
|
+
|
|
89
|
+
Install skills from remote sources.
|
|
90
|
+
|
|
91
|
+
**Supported source formats:**
|
|
92
|
+
- `owner/repo` — Clone GitHub repo, install all discovered skills
|
|
93
|
+
- `owner/repo@skill-name` — Install specific skill from GitHub repo
|
|
94
|
+
- `./local-path` — Install from local directory
|
|
95
|
+
|
|
96
|
+
**Install flow:**
|
|
97
|
+
1. Parse source string
|
|
98
|
+
2. For GitHub: `git clone --depth 1` to temp dir (60s timeout)
|
|
99
|
+
3. Discover SKILL.md files (scans `*/SKILL.md` and `skills/*/SKILL.md`)
|
|
100
|
+
4. Copy to `~/.claude/skills/{name}/SKILL.md` (ecosystem standard format)
|
|
101
|
+
5. Add `_prjct` metadata block to frontmatter (sourceUrl, sha, installedAt)
|
|
102
|
+
6. Update lock file at `~/.prjct-cli/skills/.skill-lock.json`
|
|
103
|
+
7. Clean up temp dir
|
|
104
|
+
|
|
105
|
+
**Example:**
|
|
106
|
+
```
|
|
107
|
+
p. skill add vercel-labs/skills
|
|
108
|
+
p. skill add my-org/custom-skills@api-designer
|
|
109
|
+
p. skill add ./my-local-skill
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Output:**
|
|
113
|
+
```
|
|
114
|
+
✅ Installed 3 skills from vercel-labs/skills
|
|
115
|
+
|
|
116
|
+
- frontend-design → ~/.claude/skills/frontend-design/SKILL.md
|
|
117
|
+
- find-skills → ~/.claude/skills/find-skills/SKILL.md
|
|
118
|
+
- code-review → ~/.claude/skills/code-review/SKILL.md
|
|
119
|
+
|
|
120
|
+
Lock file updated: ~/.prjct-cli/skills/.skill-lock.json
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Remove Skill (`/p:skill remove <name>`)
|
|
124
|
+
|
|
125
|
+
1. Remove skill directory from `~/.claude/skills/{name}/`
|
|
126
|
+
2. Also remove flat file if it exists (`~/.claude/skills/{name}.md`)
|
|
127
|
+
3. Remove entry from lock file
|
|
128
|
+
4. Confirm removal
|
|
129
|
+
|
|
130
|
+
**Output:**
|
|
131
|
+
```
|
|
132
|
+
✅ Removed skill: frontend-design
|
|
133
|
+
|
|
134
|
+
Deleted: ~/.claude/skills/frontend-design/
|
|
135
|
+
Lock file updated.
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Init Skill (`/p:skill init <name>`)
|
|
139
|
+
|
|
140
|
+
Scaffold a new skill in the project.
|
|
141
|
+
|
|
142
|
+
1. Create `.prjct/skills/{name}/SKILL.md` with template frontmatter
|
|
143
|
+
2. Open for editing
|
|
144
|
+
|
|
145
|
+
**Template:**
|
|
146
|
+
```markdown
|
|
147
|
+
---
|
|
148
|
+
name: {name}
|
|
149
|
+
description: TODO - describe what this skill does
|
|
150
|
+
agent: general
|
|
151
|
+
tags: []
|
|
152
|
+
version: 1.0.0
|
|
153
|
+
author: {detected-author}
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
# {Name} Skill
|
|
157
|
+
|
|
158
|
+
## Purpose
|
|
159
|
+
|
|
160
|
+
Describe what this skill helps with.
|
|
161
|
+
|
|
162
|
+
## Instructions
|
|
163
|
+
|
|
164
|
+
Step-by-step instructions for the AI agent...
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Check Updates (`/p:skill check`)
|
|
168
|
+
|
|
169
|
+
Compare lock file SHAs with remote repositories to detect available updates.
|
|
170
|
+
|
|
171
|
+
1. Read lock file entries
|
|
172
|
+
2. For each GitHub-sourced skill, run `git ls-remote` to get latest SHA
|
|
173
|
+
3. Compare with stored SHA
|
|
174
|
+
4. Report skills with available updates (no auto-update)
|
|
175
|
+
|
|
176
|
+
**Output:**
|
|
177
|
+
```
|
|
178
|
+
## Skill Update Check
|
|
179
|
+
|
|
180
|
+
- **frontend-design** (vercel-labs/skills) — Update available
|
|
181
|
+
Current: abc123 → Latest: def456
|
|
182
|
+
- **code-review** (vercel-labs/skills) — Up to date
|
|
183
|
+
- **my-local-skill** (local) — Skipped (local source)
|
|
184
|
+
|
|
185
|
+
1 update available. Run `p. skill add <source>` to update.
|
|
186
|
+
```
|
|
187
|
+
|
|
77
188
|
## Skill File Format
|
|
78
189
|
|
|
79
|
-
Skills are markdown files with frontmatter:
|
|
190
|
+
Skills are markdown files with frontmatter. Two formats are supported:
|
|
191
|
+
|
|
192
|
+
### Subdirectory Format (Ecosystem Standard)
|
|
193
|
+
```
|
|
194
|
+
~/.claude/skills/my-skill/SKILL.md
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Flat Format (Legacy)
|
|
198
|
+
```
|
|
199
|
+
~/.claude/skills/my-skill.md
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Frontmatter Schema
|
|
80
203
|
|
|
81
204
|
```markdown
|
|
82
205
|
---
|
|
@@ -85,6 +208,13 @@ description: What the skill does
|
|
|
85
208
|
agent: general
|
|
86
209
|
tags: [tag1, tag2]
|
|
87
210
|
version: 1.0.0
|
|
211
|
+
author: Author Name
|
|
212
|
+
category: development
|
|
213
|
+
_prjct:
|
|
214
|
+
sourceUrl: https://github.com/owner/repo
|
|
215
|
+
sourceType: github
|
|
216
|
+
installedAt: 2026-01-28T12:00:00.000Z
|
|
217
|
+
sha: abc123
|
|
88
218
|
---
|
|
89
219
|
|
|
90
220
|
# Skill Content
|
|
@@ -94,11 +224,30 @@ The actual prompt/instructions...
|
|
|
94
224
|
|
|
95
225
|
## Creating Custom Skills
|
|
96
226
|
|
|
97
|
-
### Project Skill
|
|
98
|
-
Create `.prjct/skills/my-skill.md`
|
|
99
|
-
|
|
100
|
-
### Global Skill
|
|
101
|
-
Create `~/.prjct-cli/skills/my-skill.md`
|
|
227
|
+
### Project Skill (repo-specific)
|
|
228
|
+
Create `.prjct/skills/my-skill/SKILL.md` or `.prjct/skills/my-skill.md`
|
|
229
|
+
|
|
230
|
+
### Global Skill (all projects)
|
|
231
|
+
Create `~/.claude/skills/my-skill/SKILL.md` or `~/.prjct-cli/skills/my-skill.md`
|
|
232
|
+
|
|
233
|
+
## Lock File
|
|
234
|
+
|
|
235
|
+
Installed skills are tracked in `~/.prjct-cli/skills/.skill-lock.json`:
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"version": 1,
|
|
240
|
+
"generatedAt": "2026-01-28T...",
|
|
241
|
+
"skills": {
|
|
242
|
+
"frontend-design": {
|
|
243
|
+
"name": "frontend-design",
|
|
244
|
+
"source": { "type": "github", "url": "vercel-labs/skills", "sha": "abc123" },
|
|
245
|
+
"installedAt": "2026-01-28T...",
|
|
246
|
+
"filePath": "~/.claude/skills/frontend-design/SKILL.md"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
102
251
|
|
|
103
252
|
## Output Format
|
|
104
253
|
|
|
@@ -106,5 +255,5 @@ Create `~/.prjct-cli/skills/my-skill.md`
|
|
|
106
255
|
## Skills ({count} total)
|
|
107
256
|
|
|
108
257
|
### {source}
|
|
109
|
-
- **{name}** ({id}): {description}
|
|
258
|
+
- **{name}** ({id}): {description} [{sourceInfo}]
|
|
110
259
|
```
|