harveyz-skill 0.5.0 → 0.6.1

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/bin/cli.js CHANGED
@@ -1,16 +1,21 @@
1
1
  #!/usr/bin/env node
2
- import { checkbox, select } from '@inquirer/prompts'
2
+ import { select } from '@inquirer/prompts'
3
3
  import chalk from 'chalk'
4
- import { execSync } from 'child_process'
4
+ import { execSync, spawnSync } from 'child_process'
5
5
  import { createRequire } from 'module'
6
+ import os from 'os'
7
+ import path from 'path'
8
+ import { fileURLToPath } from 'url'
6
9
  import {
7
- buildAllChoices, getAllSkillItems, getAllToolItems,
10
+ getAllSkillItems, getAllToolItems,
11
+ checkInstalled, checkToolInstalled, scopeSummary,
8
12
  resolveSkills, resolveSkillsByName, resolveTools, resolveToolsByName,
9
13
  TOOL_BUNDLE_CHOICES,
10
14
  } from '../lib/bundles.js'
11
15
  import { buildTargetChoices, resolveTargets, TARGETS } from '../lib/targets.js'
12
16
  import { installSkills, installTools } from '../lib/installer.js'
13
17
 
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
14
19
  const require = createRequire(import.meta.url)
15
20
  const { version } = require('../package.json')
16
21
 
@@ -32,15 +37,20 @@ function printHelp() {
32
37
  hskill install --scope <s> set scope: user (default) or project
33
38
  hskill install --force overwrite existing installs
34
39
  hskill list list available skills and bundles
40
+ hskill status show install status for all skills and tools
41
+ hskill outdated list skills and tools with available updates
42
+ hskill info <name> show install detail for a skill or tool
35
43
  hskill update update hskill to the latest version
36
- hskill --version show version
44
+ hskill version show version
37
45
  hskill --help show this help
38
46
 
39
47
  ${chalk.cyan('Examples:')}
40
48
  hskill install --bundle dev --target claude
41
49
  hskill install --skill git-workflow-init --target claude --scope project
42
50
  hskill install --tool p-launch
43
- hskill update
51
+ hskill status
52
+ hskill outdated
53
+ hskill info git-workflow-init
44
54
  `)
45
55
  }
46
56
 
@@ -49,7 +59,7 @@ if (args[0] === '--help' || args[0] === '-h') {
49
59
  process.exit(0)
50
60
  }
51
61
 
52
- if (args[0] === '--version' || args[0] === '-v') {
62
+ if (args[0] === '--version' || args[0] === '-v' || subcommand === 'version') {
53
63
  console.log(version)
54
64
  process.exit(0)
55
65
  }
@@ -87,6 +97,155 @@ if (subcommand === 'list') {
87
97
  process.exit(0)
88
98
  }
89
99
 
100
+ // ── Status / Outdated ─────────────────────────────────────────────────────────
101
+ if (subcommand === 'status' || subcommand === 'outdated') {
102
+ const outdatedOnly = subcommand === 'outdated'
103
+ const skillItems = getAllSkillItems()
104
+ const toolItems = getAllToolItems()
105
+
106
+ function icon(status) {
107
+ if (status === 'up-to-date') return chalk.green('✓')
108
+ if (status === 'update') return chalk.yellow('↑')
109
+ return chalk.dim('—')
110
+ }
111
+
112
+ const skillRows = skillItems.map(s => {
113
+ const inst = checkInstalled(s.skillName, s.version ?? '—')
114
+ return {
115
+ name: s.skillName, version: s.version ?? '—',
116
+ userStatus: scopeSummary(inst.user), projectStatus: scopeSummary(inst.project),
117
+ userDetail: inst.user, projectDetail: inst.project,
118
+ }
119
+ })
120
+ const toolRows = toolItems.map(t => {
121
+ const inst = checkToolInstalled(t.toolName, t.srcPath)
122
+ return { name: t.toolName, version: t.version ?? '—', ...inst }
123
+ })
124
+
125
+ if (outdatedOnly) {
126
+ const outdatedSkills = skillRows.filter(r => r.userStatus === 'update' || r.projectStatus === 'update')
127
+ const outdatedTools = toolRows.filter(r => r.status === 'update')
128
+
129
+ if (!outdatedSkills.length && !outdatedTools.length) {
130
+ console.log(chalk.green.bold('\n ✓ All installed skills and tools are up to date\n'))
131
+ process.exit(0)
132
+ }
133
+
134
+ const targets = ['claude', 'cursor', 'codex']
135
+
136
+ if (outdatedSkills.length) {
137
+ console.log('\n ' + chalk.bold('SKILLS WITH UPDATES'))
138
+ const nw = Math.max(...outdatedSkills.map(r => r.name.length))
139
+ for (const r of outdatedSkills) {
140
+ console.log(' ' + r.name.padEnd(nw) + ' available: ' + chalk.yellow(r.version))
141
+ for (const scope of ['user', 'project']) {
142
+ const detail = scope === 'user' ? r.userDetail : r.projectDetail
143
+ for (const t of targets) {
144
+ if (detail[t].status === 'update') {
145
+ console.log(' ' + chalk.dim(scope.padEnd(8) + t.padEnd(8)) + detail[t].version + ' → ' + chalk.yellow(r.version))
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ if (outdatedTools.length) {
153
+ console.log('\n ' + chalk.bold('TOOLS WITH UPDATES'))
154
+ const nw = Math.max(...outdatedTools.map(r => r.name.length))
155
+ for (const r of outdatedTools) {
156
+ console.log(' ' + r.name.padEnd(nw) + ' ' + chalk.dim(r.version) + ' → ' + chalk.yellow(r.version))
157
+ }
158
+ }
159
+ console.log('')
160
+ process.exit(0)
161
+ }
162
+
163
+ // Full status table
164
+ const allNames = [...skillRows, ...toolRows].map(r => r.name)
165
+ const allVers = [...skillRows, ...toolRows].map(r => r.version)
166
+ const nw = Math.max(...allNames.map(n => n.length), 4)
167
+ const vw = Math.max(...allVers.map(v => v.length), 7)
168
+ const sep = chalk.dim(' ' + '─'.repeat(nw + vw + 20))
169
+
170
+ console.log('')
171
+ console.log(' ' + chalk.bold('SKILLS') + chalk.dim(` — ${skillRows.length} available`))
172
+ console.log(sep)
173
+ console.log(' ' + ''.padEnd(nw + vw + 3) + chalk.dim('user project'))
174
+ for (const r of skillRows) {
175
+ const u = icon(r.userStatus), p = icon(r.projectStatus)
176
+ console.log(' ' + r.name.padEnd(nw) + ' ' + chalk.dim(r.version.padEnd(vw)) + ' ' + u + ' ' + p)
177
+ }
178
+
179
+ console.log('')
180
+ console.log(' ' + chalk.bold('TOOLS') + chalk.dim(` — ${toolRows.length} available`))
181
+ console.log(sep)
182
+ for (const r of toolRows) {
183
+ console.log(' ' + r.name.padEnd(nw) + ' ' + chalk.dim(r.version.padEnd(vw)) + ' ' + icon(r.status))
184
+ }
185
+
186
+ const installedSkills = skillRows.filter(r => r.userStatus !== 'none' || r.projectStatus !== 'none').length
187
+ const installedTools = toolRows.filter(r => r.status !== 'none').length
188
+ const outdatedCount = skillRows.filter(r => r.userStatus === 'update' || r.projectStatus === 'update').length
189
+ + toolRows.filter(r => r.status === 'update').length
190
+ const outdatedNote = outdatedCount ? ' · ' + chalk.yellow(outdatedCount + ' outdated') : ''
191
+ console.log('')
192
+ console.log(chalk.dim(` ${installedSkills} of ${skillRows.length} skills installed · ${installedTools} of ${toolRows.length} tools installed${outdatedNote}`))
193
+ console.log('')
194
+ process.exit(0)
195
+ }
196
+
197
+ // ── Info ──────────────────────────────────────────────────────────────────────
198
+ if (subcommand === 'info') {
199
+ const name = args[1]
200
+ if (!name) {
201
+ console.error(chalk.red(' ✗ Usage: hskill info <skill-or-tool-name>'))
202
+ process.exit(1)
203
+ }
204
+
205
+ const skillItems = getAllSkillItems()
206
+ const toolItems = getAllToolItems()
207
+ const skill = skillItems.find(s => s.skillName === name)
208
+ const tool = toolItems.find(t => t.toolName === name)
209
+
210
+ if (!skill && !tool) {
211
+ console.error(chalk.red(` ✗ Unknown: "${name}"`))
212
+ process.exit(1)
213
+ }
214
+
215
+ const targets = ['claude', 'cursor', 'codex']
216
+
217
+ function statusLabel(status) {
218
+ if (status === 'up-to-date') return chalk.green('✓ up to date')
219
+ if (status === 'update') return chalk.yellow('↑ update available')
220
+ return chalk.dim('— not installed')
221
+ }
222
+
223
+ console.log('')
224
+ if (skill) {
225
+ const inst = checkInstalled(skill.skillName, skill.version ?? '—')
226
+ console.log(' ' + chalk.bold(skill.skillName) + chalk.dim(' skill'))
227
+ console.log(' ' + chalk.dim('available: ') + (skill.version ?? '—'))
228
+ console.log('')
229
+ for (const [scopeLabel, detail] of [['USER LEVEL', inst.user], ['PROJECT LEVEL', inst.project]]) {
230
+ console.log(' ' + chalk.bold(scopeLabel))
231
+ for (const t of targets) {
232
+ const { version: v, status } = detail[t]
233
+ console.log(' ' + t.padEnd(8) + chalk.dim(v.padEnd(10)) + statusLabel(status))
234
+ }
235
+ console.log('')
236
+ }
237
+ } else {
238
+ const inst = checkToolInstalled(tool.toolName, tool.srcPath)
239
+ console.log(' ' + chalk.bold(tool.toolName) + chalk.dim(' shell tool'))
240
+ console.log(' ' + chalk.dim('available: ') + (tool.version ?? '—'))
241
+ console.log('')
242
+ console.log(' ' + chalk.bold('INSTALL STATUS'))
243
+ console.log(' ' + '~/.local/bin'.padEnd(14) + chalk.dim(inst.version.padEnd(10)) + statusLabel(inst.status))
244
+ console.log('')
245
+ }
246
+ process.exit(0)
247
+ }
248
+
90
249
  // ── Install ───────────────────────────────────────────────────────────────────
91
250
  // subcommand is 'install' or omitted (default behavior)
92
251
  const installArgs = subcommand === 'install' ? args.slice(1) : args
@@ -105,19 +264,76 @@ const scopeArg = scopeIdx !== -1 ? installArgs[scopeIdx + 1] : undefined
105
264
 
106
265
  const TOOL_BUNDLE_VALUES = new Set(TOOL_BUNDLE_CHOICES.map(c => c.value))
107
266
 
108
- function buildInteractiveChoices() {
109
- const toolItems = getAllToolItems()
110
- return [
111
- ...buildAllChoices(),
112
- ...(toolItems.length > 0
113
- ? [
114
- { type: 'separator', separator: '── shell tools ──' },
115
- ...toolItems.map(t => ({ name: t.toolName, value: t })),
116
- ]
117
- : []),
118
- { type: 'separator', separator: '────────────────' },
119
- { name: 'all skills', value: 'all' },
267
+ // fzf 交互式选择 skill/tool,返回选中的 item 列表
268
+ function fzfSelect() {
269
+ const skillItems = getAllSkillItems()
270
+ const toolItems = getAllToolItems()
271
+ const previewPath = path.join(__dirname, 'preview.mjs')
272
+
273
+ // 构建 fzf 输入:每行 "NAME\tVERSION\tBUNDLE\tKIND\tSRCPATH"
274
+ const lines = [
275
+ ...skillItems.map(s => {
276
+ const bundle = s.srcPath.split('/').slice(-2, -1)[0]
277
+ return `${s.skillName}\t${s.version ?? ''}\t${bundle}\tskill\t${s.srcPath}`
278
+ }),
279
+ ...toolItems.map(t => `${t.toolName}\t${t.version ?? '—'}\tshell-tool\ttool\t${t.srcPath}`),
120
280
  ]
281
+
282
+ const nameWidth = Math.max(...lines.map(l => l.split('\t')[0].length))
283
+ const versionWidth = Math.max(...lines.map(l => l.split('\t')[1].length))
284
+
285
+ const G = '\x1b[32m', Y = '\x1b[33m', D = '\x1b[2m', R = '\x1b[0m'
286
+ function colorIcon(status) {
287
+ if (status === 'up-to-date') return G + '✓' + R
288
+ if (status === 'update') return Y + '↑' + R
289
+ return D + '—' + R
290
+ }
291
+
292
+ // fzf 展示格式:NAME VERSION U:? P:? BUNDLE
293
+ const displayLines = lines.map(l => {
294
+ const [name, ver, bundle, kind, srcPath] = l.split('\t')
295
+ let uIcon = D + '—' + R, pIcon = D + '—' + R
296
+ if (kind === 'skill') {
297
+ const installed = checkInstalled(name, ver)
298
+ uIcon = colorIcon(scopeSummary(installed.user))
299
+ pIcon = colorIcon(scopeSummary(installed.project))
300
+ } else if (kind === 'tool') {
301
+ uIcon = colorIcon(checkToolInstalled(name, srcPath).status)
302
+ }
303
+ return `${name.padEnd(nameWidth)} ${ver.padEnd(versionWidth)} U:${uIcon} P:${pIcon} ${bundle}`
304
+ })
305
+
306
+ // 把原始数据附在末尾(隐藏列,用于解析和 preview)
307
+ const fzfInput = displayLines.map((d, i) => `${d}\t${lines[i]}`).join('\n')
308
+
309
+ const header = ` hskill · U=user P=project ${G}✓${R}=ok ${Y}↑${R}=update ${D}—${R}=none · tab 多选 · enter 确认`
310
+
311
+ const result = spawnSync('fzf', [
312
+ '--multi',
313
+ '--ansi',
314
+ '--delimiter=\t',
315
+ '--with-nth=1',
316
+ '--prompt= › ',
317
+ `--header=${header}`,
318
+ '--layout=reverse',
319
+ '--border=rounded',
320
+ '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
321
+ `--preview=node ${previewPath} {2} {3} {5} {6}`,
322
+ '--preview-window=right:42%:wrap',
323
+ ], {
324
+ input: fzfInput,
325
+ encoding: 'utf8',
326
+ stdio: ['pipe', 'pipe', 'inherit'],
327
+ })
328
+
329
+ if (result.status !== 0 || !result.stdout.trim()) return []
330
+
331
+ return result.stdout.trim().split('\n').map(line => {
332
+ const parts = line.split('\t')
333
+ const [, name, ver, bundle, kind, srcPath] = parts
334
+ if (kind === 'skill') return { kind: 'skill', skillName: name, srcPath, version: ver }
335
+ return { kind: 'tool', toolName: name, srcPath, version: ver }
336
+ })
121
337
  }
122
338
 
123
339
  try {
@@ -137,24 +353,17 @@ try {
137
353
  if (skillBundles.length) skillItems = resolveSkills(skillBundles).map(s => ({ kind: 'skill', ...s }))
138
354
  if (toolBundles.length) toolItems = resolveTools(toolBundles).map(t => ({ kind: 'tool', ...t }))
139
355
  } else {
140
- const selected = await checkbox({
141
- message: 'Select items to install (space to select, enter to confirm):',
142
- choices: buildInteractiveChoices(),
143
- })
356
+ const selected = fzfSelect()
144
357
 
145
358
  if (!selected.length) {
146
359
  console.log(chalk.dim(' · Nothing selected, exiting'))
147
360
  process.exit(0)
148
361
  }
149
362
 
150
- const hasAll = selected.includes('all')
151
- const items = selected.filter(s => s !== 'all')
152
-
153
- const selectedSkills = hasAll ? getAllSkillItems() : items.filter(s => s.kind === 'skill')
154
- toolItems = items.filter(s => s.kind === 'tool')
363
+ toolItems = selected.filter(s => s.kind === 'tool')
155
364
 
156
365
  const seen = new Set()
157
- skillItems = selectedSkills.filter(s => {
366
+ skillItems = selected.filter(s => s.kind === 'skill').filter(s => {
158
367
  if (seen.has(s.skillName)) return false
159
368
  seen.add(s.skillName); return true
160
369
  })
@@ -170,13 +379,22 @@ try {
170
379
  // Resolve scope
171
380
  let scope = scopeArg ?? 'user'
172
381
  if (!scopeArg && !targetArg) {
173
- scope = await select({
174
- message: 'Scope:',
175
- choices: [
176
- { name: `user — ~/.claude/skills/ (shared across all projects)`, value: 'user' },
177
- { name: `project — .claude/skills/ (current project only)`, value: 'project' },
178
- ],
382
+ const scopeResult = spawnSync('fzf', [
383
+ '--prompt= › ',
384
+ '--header= Scope · enter 确认 · esc 取消',
385
+ '--layout=reverse',
386
+ '--border=rounded',
387
+ '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
388
+ ], {
389
+ input: `user — ~/.claude/skills/ (所有项目共享)\nproject — .claude/skills/ (仅当前项目)`,
390
+ encoding: 'utf8',
391
+ stdio: ['pipe', 'pipe', 'inherit'],
179
392
  })
393
+ if (!scopeResult.stdout.trim()) {
394
+ console.log(chalk.dim(' · Cancelled'))
395
+ process.exit(0)
396
+ }
397
+ scope = scopeResult.stdout.trim().startsWith('project') ? 'project' : 'user'
180
398
  }
181
399
 
182
400
  // Resolve target
@@ -184,13 +402,26 @@ try {
184
402
  if (targetArg) {
185
403
  selectedTargets = targetArg === 'all' ? ['claude', 'cursor', 'codex'] : [targetArg]
186
404
  } else {
187
- selectedTargets = await checkbox({
188
- message: 'Install to which tools (space to select, enter to confirm):',
189
- choices: [
190
- ...buildTargetChoices(scope),
191
- { name: 'all — all tools', value: 'all' },
192
- ],
405
+ const targetChoices = buildTargetChoices(scope)
406
+ const targetInput = targetChoices.map(c => c.name).join('\n') + '\nall — all tools'
407
+ const targetResult = spawnSync('fzf', [
408
+ '--multi',
409
+ '--prompt= › ',
410
+ '--header= Install to · tab 多选 · enter 确认 · esc 取消',
411
+ '--layout=reverse',
412
+ '--border=rounded',
413
+ '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
414
+ ], {
415
+ input: targetInput,
416
+ encoding: 'utf8',
417
+ stdio: ['pipe', 'pipe', 'inherit'],
193
418
  })
419
+ if (!targetResult.stdout.trim()) {
420
+ console.log(chalk.dim(' · Cancelled'))
421
+ process.exit(0)
422
+ }
423
+ selectedTargets = targetResult.stdout.trim().split('\n')
424
+ .map(l => l.trim().split(/\s+/)[0])
194
425
  }
195
426
 
196
427
  if (selectedTargets.length > 0) {
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs'
3
+ import os from 'os'
4
+ import path from 'path'
5
+ import { fileURLToPath } from 'url'
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
+
9
+ const skillName = process.argv[2]?.trim()
10
+ const availableVersion = process.argv[3]?.trim() || '—'
11
+ const kind = process.argv[4]?.trim()
12
+ const srcPath = process.argv[5]?.trim()
13
+
14
+ const G = '\x1b[32m', Y = '\x1b[33m', D = '\x1b[2m', R = '\x1b[0m', B = '\x1b[1m'
15
+
16
+ if (!skillName) process.exit(0)
17
+
18
+ if (kind !== 'skill') {
19
+ const home = os.homedir()
20
+ const installedBin = path.join(home, '.local', 'bin', skillName)
21
+ const installedMeta = path.join(home, '.local', 'share', 'hskill', 'tools', `${skillName}.json`)
22
+
23
+ function readToolMeta(jsonPath) {
24
+ try {
25
+ const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
26
+ return data.version ?? '—'
27
+ } catch { return '—' }
28
+ }
29
+
30
+ const installed = fs.existsSync(installedBin)
31
+ const installedVersion = installed ? readToolMeta(installedMeta) : '—'
32
+ const sourceVersion = srcPath ? readToolMeta(path.join(srcPath, 'tool.json')) : availableVersion
33
+ const status = !installed ? 'none'
34
+ : installedVersion === '—' || installedVersion === sourceVersion ? 'up-to-date'
35
+ : 'update'
36
+
37
+ function statusLine(version, st) {
38
+ const ver = version.padEnd(8)
39
+ if (st === 'up-to-date') return ver + ' ' + G + '✓ up to date' + R
40
+ if (st === 'update') return ver + ' ' + Y + '↑ update available' + R
41
+ return ver + ' ' + D + '— not installed' + R
42
+ }
43
+
44
+ console.log(B + skillName + R)
45
+ console.log(D + 'available: ' + R + sourceVersion)
46
+ console.log('')
47
+ console.log(B + 'INSTALL STATUS' + R)
48
+ console.log(' ' + '~/.local/bin'.padEnd(14) + ' ' + statusLine(installedVersion, status))
49
+ console.log('')
50
+ if (status === 'update') {
51
+ console.log(Y + 'ACTION: update → ~/.local/bin' + R)
52
+ } else if (status === 'none') {
53
+ console.log(D + 'STATUS: not installed' + R)
54
+ } else {
55
+ console.log(G + 'STATUS: ok' + R)
56
+ }
57
+ process.exit(0)
58
+ }
59
+
60
+ function readVersion(skillPath) {
61
+ try {
62
+ const content = fs.readFileSync(path.join(skillPath, 'SKILL.md'), 'utf8')
63
+ const m = content.match(/^---[\s\S]*?^version:\s*["']?([^"'\n]+)["']?/m)
64
+ return m ? m[1].trim() : '—'
65
+ } catch { return '—' }
66
+ }
67
+
68
+ function statusLine(version, status) {
69
+ const ver = version.padEnd(8)
70
+ if (status === 'up-to-date') return ver + ' ' + G + '✓ up to date' + R
71
+ if (status === 'update') return ver + ' ' + Y + '↑ update available' + R
72
+ return ver + ' ' + D + '— not installed' + R
73
+ }
74
+
75
+ const targets = ['claude', 'cursor', 'codex']
76
+ const home = os.homedir()
77
+
78
+ function checkScope(dirFn) {
79
+ return targets.map(t => {
80
+ const ver = readVersion(path.join(dirFn(t), skillName))
81
+ const status = ver === '—' ? 'none'
82
+ : ver === availableVersion ? 'up-to-date'
83
+ : 'update'
84
+ return { tool: t, version: ver, status }
85
+ })
86
+ }
87
+
88
+ const cwd = process.cwd()
89
+ const userDetails = checkScope(t => path.join(home, `.${t}`, 'skills'))
90
+ const projectDetails = cwd === home
91
+ ? targets.map(t => ({ tool: t, version: '—', status: 'none' }))
92
+ : checkScope(t => path.join(cwd, `.${t}`, 'skills'))
93
+
94
+ console.log(B + skillName + R)
95
+ console.log(D + 'available: ' + R + availableVersion)
96
+ console.log('')
97
+
98
+ console.log(B + 'USER LEVEL' + R)
99
+ for (const { tool, version, status } of userDetails) {
100
+ console.log(' ' + tool.padEnd(8) + statusLine(version, status))
101
+ }
102
+ console.log('')
103
+
104
+ console.log(B + 'PROJECT LEVEL' + R)
105
+ for (const { tool, version, status } of projectDetails) {
106
+ console.log(' ' + tool.padEnd(8) + statusLine(version, status))
107
+ }
108
+ console.log('')
109
+
110
+ const allDetails = [...userDetails, ...projectDetails]
111
+ const needsUpdate = allDetails.filter(d => d.status === 'update')
112
+ const allNone = allDetails.every(d => d.status === 'none')
113
+
114
+ if (needsUpdate.length > 0) {
115
+ const tools = [...new Set(needsUpdate.map(d => d.tool))].join(', ')
116
+ console.log(Y + 'ACTION: update → ' + tools + R)
117
+ } else if (allNone) {
118
+ console.log(D + 'STATUS: not installed' + R)
119
+ } else {
120
+ console.log(G + 'STATUS: ok' + R)
121
+ }
package/lib/bundles.js CHANGED
@@ -1,3 +1,5 @@
1
+ import fs from 'fs'
2
+ import os from 'os'
1
3
  import { createRequire } from 'module'
2
4
  import path from 'path'
3
5
  import { fileURLToPath } from 'url'
@@ -9,6 +11,23 @@ const { bundleMeta, skills: skillDefs, toolBundleMeta = {}, tools: toolDefs = []
9
11
  const repoRoot = path.join(__dirname, '..')
10
12
  const skillsRoot = path.join(repoRoot, 'skills')
11
13
 
14
+ // 从 SKILL.md frontmatter 读取 version 字段
15
+ function readVersion(skillPath) {
16
+ try {
17
+ const content = fs.readFileSync(path.join(skillPath, 'SKILL.md'), 'utf8')
18
+ const m = content.match(/^---[\s\S]*?^version:\s*["']?([^"'\n]+)["']?/m)
19
+ return m ? m[1].trim() : '—'
20
+ } catch {
21
+ return '—'
22
+ }
23
+ }
24
+
25
+ // 格式化 checkbox 展示名:左对齐 skill 名,右侧对齐版本号
26
+ function formatChoice(name, version, nameWidth) {
27
+ const padded = name.padEnd(nameWidth)
28
+ return `${padded} ${version}`
29
+ }
30
+
12
31
  // ── Skills ──────────────────────────────────────────────────────────────────
13
32
  const bundleSkills = {}
14
33
  for (const skill of skillDefs) {
@@ -57,14 +76,21 @@ export function resolveSkills(selectedBundles) {
57
76
  export function buildAllChoices() {
58
77
  const choices = []
59
78
 
79
+ // 预计算所有 skill 名的最大长度,统一对齐
80
+ const allSkillNames = skillDefs.map(s => s.path.split('/').pop())
81
+ const allToolNames = toolDefs.map(t => t.name)
82
+ const nameWidth = Math.max(...[...allSkillNames, ...allToolNames].map(n => n.length))
83
+
60
84
  // ── skill ── 分组
61
85
  for (const [bundleName, skills] of Object.entries(bundleSkills)) {
62
86
  choices.push({ type: 'separator', separator: `── ${bundleName} ──` })
63
87
  for (const skill of skills) {
64
88
  const skillName = skill.path.split('/').pop()
89
+ const srcPath = path.join(skillsRoot, skill.path)
90
+ const version = readVersion(srcPath)
65
91
  choices.push({
66
- name: skillName,
67
- value: { kind: 'skill', skillName, srcPath: path.join(skillsRoot, skill.path) },
92
+ name: formatChoice(skillName, version, nameWidth),
93
+ value: { kind: 'skill', skillName, srcPath },
68
94
  })
69
95
  }
70
96
  }
@@ -74,20 +100,90 @@ export function buildAllChoices() {
74
100
 
75
101
  // 所有 skill 展开为 { kind:'skill', skillName, srcPath } 列表(对应 all 选项)
76
102
  export function getAllSkillItems() {
77
- return skillDefs.map(skill => ({
78
- kind: 'skill',
79
- skillName: skill.path.split('/').pop(),
80
- srcPath: path.join(skillsRoot, skill.path),
81
- }))
103
+ return skillDefs.map(skill => {
104
+ const srcPath = path.join(skillsRoot, skill.path)
105
+ return {
106
+ kind: 'skill',
107
+ skillName: skill.path.split('/').pop(),
108
+ srcPath,
109
+ version: readVersion(srcPath),
110
+ }
111
+ })
82
112
  }
83
113
 
84
- // 所有 tool 展开为 { kind:'tool', toolName, srcPath } 列表
114
+ // 所有 tool 展开为 { kind:'tool', toolName, srcPath, version } 列表
85
115
  export function getAllToolItems() {
86
- return toolDefs.map(tool => ({
87
- kind: 'tool',
88
- toolName: tool.name,
89
- srcPath: path.join(repoRoot, 'tools', tool.name),
90
- }))
116
+ return toolDefs.map(tool => {
117
+ const srcPath = path.join(repoRoot, 'tools', tool.name)
118
+ return {
119
+ kind: 'tool',
120
+ toolName: tool.name,
121
+ srcPath,
122
+ version: readToolMeta(path.join(srcPath, 'tool.json')),
123
+ }
124
+ })
125
+ }
126
+
127
+ // 检查某个 skill 在 user/project 级别每个工具的安装状态
128
+ // 返回 { user: { claude, cursor, codex }, project: { claude, cursor, codex } }
129
+ // 每个工具: { version: string, status: 'up-to-date'|'update'|'none' }
130
+ export function checkInstalled(skillName, availableVersion) {
131
+ const home = os.homedir()
132
+ const targets = ['claude', 'cursor', 'codex']
133
+
134
+ function checkScope(dirFn) {
135
+ const result = {}
136
+ for (const t of targets) {
137
+ const skillPath = path.join(dirFn(t), skillName)
138
+ const version = readVersion(skillPath)
139
+ const status = version === '—' ? 'none'
140
+ : version === availableVersion ? 'up-to-date'
141
+ : 'update'
142
+ result[t] = { version, status }
143
+ }
144
+ return result
145
+ }
146
+
147
+ const cwd = process.cwd()
148
+ const noneScope = Object.fromEntries(targets.map(t => [t, { version: '—', status: 'none' }]))
149
+
150
+ return {
151
+ user: checkScope(t => path.join(home, `.${t}`, 'skills')),
152
+ project: cwd === home ? noneScope : checkScope(t => path.join(cwd, `.${t}`, 'skills')),
153
+ }
154
+ }
155
+
156
+ // 将 scope detail 聚合为单一摘要状态
157
+ export function scopeSummary(scopeDetail) {
158
+ const statuses = Object.values(scopeDetail).map(t => t.status)
159
+ if (statuses.some(s => s === 'update')) return 'update'
160
+ if (statuses.some(s => s === 'up-to-date')) return 'up-to-date'
161
+ return 'none'
162
+ }
163
+
164
+ export { formatChoice, readVersion }
165
+
166
+ const HSKILL_TOOLS_DATA = path.join(os.homedir(), '.local', 'share', 'hskill', 'tools')
167
+
168
+ function readToolMeta(jsonPath) {
169
+ try {
170
+ const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
171
+ return data.version ?? '—'
172
+ } catch { return '—' }
173
+ }
174
+
175
+ // 检测工具安装状态:binary 存在于 ~/.local/bin,版本从安装时写入的 data JSON 读取
176
+ export function checkToolInstalled(toolName, srcPath) {
177
+ const installedBin = path.join(os.homedir(), '.local', 'bin', toolName)
178
+ const installedMeta = path.join(HSKILL_TOOLS_DATA, `${toolName}.json`)
179
+ try {
180
+ if (!fs.existsSync(installedBin)) return { version: '—', status: 'none' }
181
+ const installedVersion = readToolMeta(installedMeta)
182
+ const sourceVersion = readToolMeta(path.join(srcPath, 'tool.json'))
183
+ const status = installedVersion === '—' || sourceVersion === '—' || installedVersion === sourceVersion
184
+ ? 'up-to-date' : 'update'
185
+ return { version: installedVersion, status }
186
+ } catch { return { version: '—', status: 'none' } }
91
187
  }
92
188
 
93
189
  // ── Tools ────────────────────────────────────────────────────────────────────
package/lib/installer.js CHANGED
@@ -69,6 +69,14 @@ export async function installTools(tools, targetDir, force = false) {
69
69
  }
70
70
  const content = await fs.readFile(scriptSrc, 'utf-8')
71
71
  await fs.writeFile(destPath, substituteVars(content, varsMap), { encoding: 'utf-8', mode: 0o755 })
72
+
73
+ const toolJsonSrc = path.join(srcPath, 'tool.json')
74
+ if (await fs.pathExists(toolJsonSrc)) {
75
+ const dataDir = path.join(os.homedir(), '.local', 'share', 'hskill', 'tools')
76
+ await fs.ensureDir(dataDir)
77
+ await fs.copy(toolJsonSrc, path.join(dataDir, `${toolName}.json`))
78
+ }
79
+
72
80
  installed.push(toolName)
73
81
  await _patchZshrc(srcPath, toolName)
74
82
  } catch (err) {
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "harveyz-skill",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Skill manager for Claude Code, Cursor, and Codex",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "hskill": "bin/cli.js"
8
8
  },
9
9
  "scripts": {
10
- "prepack": "node scripts/generate-npmignore.js"
10
+ "prepack": "node scripts/generate-npmignore.js",
11
+ "test": "bats tests/ tools/p-launch/tests/"
11
12
  },
12
13
  "files": [
13
14
  "bin/",
@@ -16,6 +17,7 @@
16
17
  "skills-index.json",
17
18
  "skills/analysis/skill-analyzer/CHANGELOG.md",
18
19
  "skills/analysis/skill-analyzer/SKILL.md",
20
+ "skills/analysis/skill-analyzer/references/",
19
21
  "skills/harness/diataxis-docs/",
20
22
  "skills/harness/full-stack-debug-env/",
21
23
  "skills/superpowers-fork/brainstorming/",
@@ -1,5 +1,22 @@
1
1
  # CHANGELOG — skill-analyzer
2
2
 
3
+ ## v1.0.0 (2026-05-12)
4
+
5
+ **基于:** v0.9 + 结构性重构
6
+
7
+ ### 主要变更
8
+
9
+ - 补全 YAML frontmatter(新增 `name`、`description`、`user_invocable`、`version` 字段)
10
+ - 新增 `references/` 目录,抽象三份模版:
11
+ - `prohibitions.md` — 完整 23 条禁忌(原 SKILL.md 仅保留第 23 条)
12
+ - `output-template.md` — 分析报告骨架模版
13
+ - `evaluation-template.md` — 评估报告骨架模版
14
+ - SKILL.md 中 `禁忌` / `输出格式` / `Layer 1-4` 三个 section 改为引用 references
15
+ - 新增 `输出路径` 规范:输出文件保存在被分析项目根目录 `skill-analysis/` 下
16
+ - 删除 `research/` 目录(历史实验产物,路径不符合输出规范)
17
+
18
+ ---
19
+
3
20
  ## v0.7 (2026-03-28)
4
21
 
5
22
  **基于:** v0.6 + 第5轮评估反馈
@@ -1,8 +1,15 @@
1
+ ---
2
+ name: skill-analyzer
3
+ description: "对 Skill 仓库进行系统性分析。触发词:分析这个 skill 仓库、对这个 skill 仓库做系统性研究、输出 skill 仓库的分析报告、理解这个 skill 系统的设计意图"
4
+ user_invocable: true
5
+ version: "1.0.0"
6
+ ---
7
+
1
8
  # SKILL.md — skill-analyzer
2
9
 
3
- > **版本:** v0.9
4
- > **版本日期:** 2026-03-28
5
- > **基于:** v0.8 + 第7轮评估反馈
10
+ > **版本:** v1.0.0
11
+ > **版本日期:** 2026-05-12
12
+ > **基于:** v0.9 + 结构性重构
6
13
  > **定位:** 对 Skill 仓库进行系统性分析的工具 Skill
7
14
 
8
15
  ---
@@ -111,40 +118,44 @@ cso, review, ship
111
118
 
112
119
  ---
113
120
 
114
- ## Layer 1-4
121
+ ## Layer 1-4 执行指南
115
122
 
116
- 按照洋葱模型框架执行。
123
+ 按洋葱模型从内到外逐层分析:
117
124
 
118
- ---
125
+ - **Layer 1 — 设计意图(哲学视角):** 读 ETHOS.md / ARCHITECTURE.md / README 等哲学文档,提炼系统定位和核心设计原则
126
+ - **Layer 2 — 组件目录(结构视角):** 实际运行命令列出所有目录和文件,统计各目录文件数,逐目录列清单,不能估算
127
+ - **Layer 3 — 交互关系(系统视角):** 识别三类关系(自动触发 / 建议序列 / 前置配置),分析 `allowed-tools` 权限矩阵(必须从实际 SKILL.md 读取)
128
+ - **Layer 4 — 使用场景(用户视角):** 提炼 3-5 个典型工作流场景,列出降级矩阵
119
129
 
120
- ## 输出格式
130
+ 报告结构详见 → `references/output-template.md`
121
131
 
122
- ```markdown
123
- # {仓库名} 系统分析报告
132
+ ---
124
133
 
125
- ## 元信息
126
- - 分析版本:
127
- - 项目类型:
128
- - VERSION / package.json / CHANGELOG:
129
- - 版本根因分析:
134
+ ## 输出路径
130
135
 
131
- ## 1-6. 各层分析
132
- ...
136
+ 所有输出文件保存在**被分析项目的根目录**下,新建 `skill-analysis/` 目录:
133
137
 
134
- ## 附录
135
- ### 幽灵文件列表(如有)
136
- ### allowed-tools 完整读取记录
137
- (每个 skill:工具数 + 具体工具名称)
138
138
  ```
139
+ {被分析项目根目录}/
140
+ └── skill-analysis/
141
+ ├── analysis/
142
+ │ └── {YYYY-MM-DD}-{仓库名}-report.md # 分析报告
143
+ └── evaluation/
144
+ └── {YYYY-MM-DD}-{仓库名}-eval.md # 评估报告(如有)
145
+ ```
146
+
147
+ 不得将输出文件保存在 skill 自身目录下。
148
+
149
+ ## 输出格式
150
+
151
+ 使用 `references/output-template.md` 中的结构填写报告,所有占位符替换为实际内容。
139
152
 
140
153
  ---
141
154
 
142
155
  ## 禁忌(共 23 条)
143
156
 
144
- 1-22 条沿用 v0.8(略)
145
-
146
- **23. ❌ WebSearch 数量必须为 12 个(不是16!)。benchmark/canary/document-release/setup-deploy 均不含 WebSearch。browse/test/ = 18(不是11)。bin/ + browse/bin/ = 19(不是20)。** ← 新增
157
+ **执行前必须读取 `references/prohibitions.md`**,每条禁忌均来自真实迭代中发现的系统性错误。
147
158
 
148
159
  ---
149
160
 
150
- *skill-analyzer v0.9 | 2026-03-28*
161
+ *skill-analyzer v1.0.0 | 2026-05-12*
@@ -0,0 +1,58 @@
1
+ # 评估报告模版
2
+
3
+ > 用于评估分析报告的质量。评估者对照源码逐项核实,填写此模版。
4
+ > 保存路径:`{被分析项目根目录}/skill-analysis/evaluation/{YYYY-MM-DD}-{仓库名}-eval.md`
5
+
6
+ ---
7
+
8
+ ```markdown
9
+ # 评估报告
10
+
11
+ **评估对象:** `{分析报告文件名}`
12
+ **源码仓库:** `{仓库路径}`
13
+ **评估时间:** {YYYY-MM-DD}
14
+ **对应 skill 版本:** {版本号}
15
+
16
+ ---
17
+
18
+ ## 遗漏项
19
+
20
+ ### {问题名称}(严重程度:高/中/低)
21
+
22
+ **现象:** {报告中写了什么}
23
+ **实际:** {源码中的真实情况}
24
+ **影响:** {如果依据此报告会导致什么错误}
25
+
26
+ ---
27
+
28
+ ## 理解偏差
29
+
30
+ ### {偏差描述}
31
+
32
+ **报告表述:** {引用原文}
33
+ **源码验证:** {实际结果}
34
+ **结论:** {是报告理解错误,还是源码本身有问题}
35
+
36
+ ---
37
+
38
+ ## 评估者无法理解的点
39
+
40
+ ### {问题}
41
+
42
+ {描述无法理解的设计决策或代码现象,供后续向 maintainer 确认}
43
+
44
+ ---
45
+
46
+ ## 总结评分
47
+
48
+ | 维度 | 评级 | 说明 |
49
+ |------|------|------|
50
+ | 版本信息核实 | ✅/⚠️/❌ | {说明} |
51
+ | 文件数量准确性 | ✅/⚠️/❌ | {说明} |
52
+ | 目录覆盖完整性 | ✅/⚠️/❌ | {说明} |
53
+ | 幽灵文件检查 | ✅/⚠️/❌ | {说明} |
54
+ | allowed-tools 准确性 | ✅/⚠️/❌ | {说明} |
55
+ | 关系类型描述 | ✅/⚠️/❌ | {说明} |
56
+
57
+ **综合结论:** {是否达到发布标准,需要哪些修正}
58
+ ```
@@ -0,0 +1,153 @@
1
+ # 分析报告输出模版
2
+
3
+ > 使用此模版填写最终报告。所有 `{占位符}` 必须替换为实际内容。
4
+ > 保存路径:`{被分析项目根目录}/skill-analysis/analysis/{YYYY-MM-DD}-{仓库名}-report.md`
5
+
6
+ ---
7
+
8
+ ```markdown
9
+ # {仓库名} 系统分析报告
10
+
11
+ ## 元信息
12
+
13
+ - **分析版本:** skill-analyzer {版本号}
14
+ - **分析时间:** {YYYY-MM-DD}
15
+ - **仓库路径:** `{路径}`
16
+ - **VERSION 文件:** `{版本号}`
17
+ - **package.json version:** `{版本号}`
18
+ - **CHANGELOG 最新版本:** `{版本号}`
19
+ - **HEAD commit:** `{hash}`
20
+
21
+ > ⚠️ **版本一致性:** {描述三者是否一致,若不一致说明根因}
22
+
23
+ ---
24
+
25
+ ## 1. 设计意图(Layer 1)
26
+
27
+ ### 系统定位
28
+
29
+ {用一句话概括:这个仓库是什么,核心资产是什么}
30
+
31
+ ### 哲学文档
32
+
33
+ - {列出 ETHOS.md / ARCHITECTURE.md / DESIGN.md 等,说明是否存在}
34
+
35
+ ### 核心设计原则
36
+
37
+ | 原则 | 含义 |
38
+ |------|------|
39
+ | {原则名} | {解释} |
40
+
41
+ ---
42
+
43
+ ## 2. 目录结构(Layer 2)
44
+
45
+ ```
46
+ {仓库名}/
47
+ ├── {文件或目录} # {说明}
48
+ ├── ...
49
+ ```
50
+
51
+ ### 幽灵文件检查
52
+
53
+ {列出所有标注存在但实际不符预期的文件/目录,若无则写"无幽灵文件"}
54
+
55
+ ---
56
+
57
+ ## 3. 组件清单(Layer 2 详细)
58
+
59
+ ### 3.1 核心组件统计(实际计数)
60
+
61
+ {根据仓库类型列出核心组件,如 skill 仓库列出所有 SKILL.md 目录}
62
+
63
+ | # | 名称 | 路径 | 类型/层级 | 功能定位 |
64
+ |---|------|------|---------|---------|
65
+ | 1 | {名称} | {路径} | {类型} | {功能} |
66
+
67
+ ### 3.2 工具权限矩阵(如适用)
68
+
69
+ {如果是 skill 仓库,列出每个 skill 的 allowed-tools,数据必须从实际文件读取}
70
+
71
+ | Skill | Bash | Read | Write | Edit | Glob | Grep | Agent | AskUserQ | WebSearch |
72
+ |-------|------|------|-------|------|------|------|-------|---------|-----------|
73
+
74
+ ### 3.3 模版文件(如适用)
75
+
76
+ {列出所有 .tmpl 文件,说明其与生成产物的关系}
77
+
78
+ ### 3.4 关键目录文件清单
79
+
80
+ {对每个重要目录,逐一列出实际文件,不能只报总数}
81
+
82
+ ---
83
+
84
+ ## 4. 交互关系(Layer 3)
85
+
86
+ ### 类型 1:自动触发(确定性)
87
+
88
+ | 关系 | 说明 |
89
+ |------|------|
90
+ | {触发方} → {被触发方} | {说明} |
91
+
92
+ ### 类型 2:建议序列(推荐顺序)
93
+
94
+ | 关系 | 说明 |
95
+ |------|------|
96
+ | {步骤序列} | {说明} |
97
+
98
+ ### 类型 3:前置配置(运行时依赖)
99
+
100
+ | 关系 | 说明 |
101
+ |------|------|
102
+ | {依赖项} | {缺少时的影响} |
103
+
104
+ ---
105
+
106
+ ## 5. 构建流水线(如适用)
107
+
108
+ ```
109
+ {源文件} → [{构建工具}] → {产物}
110
+ ```
111
+
112
+ {说明 CI 如何保障构建产物与源文件一致}
113
+
114
+ ---
115
+
116
+ ## 6. 使用场景(Layer 4)
117
+
118
+ ### 典型场景
119
+
120
+ #### 场景 A:{场景名}({频率})
121
+
122
+ ```
123
+ {用户输入} → {步骤1} → {步骤2} → {结果}
124
+ ```
125
+
126
+ **覆盖组件:** {组件列表}
127
+
128
+ ### 降级场景矩阵
129
+
130
+ | 组件不可用 | 降级行为 |
131
+ |-----------|---------|
132
+ | {组件} | {降级方式} |
133
+
134
+ ---
135
+
136
+ ## 附录
137
+
138
+ ### 幽灵文件列表
139
+
140
+ | 文件路径 | 状态 | 说明 |
141
+ |---------|------|------|
142
+ | {路径} | {✅/⚠️/❌} | {说明} |
143
+
144
+ ### 版本信息不一致分析(如存在)
145
+
146
+ | 来源 | 版本号 | 说明 |
147
+ |------|-------|------|
148
+ | VERSION 文件 | {版本} | {说明} |
149
+ | package.json | {版本} | {说明} |
150
+ | CHANGELOG | {版本} | {说明} |
151
+
152
+ **根因:** {推断原因}
153
+ ```
@@ -0,0 +1,27 @@
1
+ # 禁忌清单(共 23 条)
2
+
3
+ > 每条禁忌均来自真实迭代中发现的系统性错误,不得违反。
4
+
5
+ 1. ❌ 不能只统计 skill 数量,必须列出完整文件清单
6
+ 2. ❌ 不能忽略 `.tmpl` 文件的 auto-generate 机制
7
+ 3. ❌ 不能混淆 CLI 子项目(如 browse/)和普通 skill
8
+ 4. ❌ 不能混用三种关系类型(自动触发 / 建议序列 / 前置配置)
9
+ 5. ❌ 不能遗漏版本信息(VERSION 文件、package.json、CHANGELOG 三者都要检查)
10
+ 6. ❌ 不能估算文件数量,必须实际运行命令计数
11
+ 7. ❌ 不能忽略 `extension/`、`lib/`、`docs/`、`.github/` 等辅助目录
12
+ 8. ❌ 不能遗漏 CI workflow(`.github/workflows/` 必须列出)
13
+ 9. ❌ `allowed-tools` 描述不能与表格数据矛盾
14
+ 10. ❌ 每个列出的文件必须验证其确实存在,不得推断
15
+ 11. ❌ `allowed-tools` 必须从实际 `SKILL.md` 文件读取,不得从 `.tmpl` 推断
16
+ 12. ❌ 不得将 `browse/bin/` 与根目录 `bin/` 混淆,两者必须明确区分
17
+ 13. ❌ 不得假设每个 skill 都有独立的 `.tmpl` 文件(通常只有少数 skill 有)
18
+ 14. ❌ 不得在未完成项目类型检测的情况下假设目标是 skill 仓库
19
+ 15. ❌ 非 skill 仓库缺失 `SKILL.md` 等文件不算幽灵文件,属于正常
20
+ 16. ❌ ML 项目必须覆盖关键实现细节(模型结构、训练逻辑等),不能套用 skill 仓库框架
21
+ 17. ❌ 每个目录必须列出实际文件清单,不能只报总数
22
+ 18. ❌ 独立 `.tmpl` 数量必须实际统计,不得假设与 skill 数量一致
23
+ 19. ❌ 版本号不一致时必须探讨根因(为什么三者不同步),不能只记录数字
24
+ 20. ❌ `allowed-tools` 数据必须从实际文件读取,不得凭记忆或推断伪造
25
+ 21. ❌ 不存在"双 `allowed-tools` 块"的合理说法,如发现应标记为异常
26
+ 22. ❌ `WebSearch` 必须完整计入工具数;注意 `unfreeze` 只有 2 个工具;`gstack-upgrade` 版本为 v1.1.0
27
+ 23. ❌ 以下数量为已验证的正确值,报告时必须与实际计数一致,不得凭记忆填写旧值
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: diataxis-docs
3
+ version: "1.0.0"
3
4
  description: Use when creating, updating, or deleting any file under docs/ — including writing a new guide, updating reference content, adding an ADR, or removing an outdated page. Also use when searching docs before writing, to avoid duplicating existing content.
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: full-stack-debug-env
3
+ version: "1.0.0"
3
4
  description: Use when setting up a log monitoring environment before running agents or test scenarios on a multi-component application — captures frontend browser console, backend processes, workers, daemons, and other services as independent log files so agents can query each layer precisely
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: pm-task-dispatch
3
+ version: "1.0.0"
3
4
  description: PM 任务派发技能。当需要创建新任务并派发给其他 Agent 时触发。流程:理解任务 → 需求澄清 → 分析细化 → 创建任务文档 → sessions_send 派发 → 追踪反馈。触发场景包括:(1)Harvey 提出新任务需求,(2)要求向特定 Agent 分派任务,(3)询问任务如何派发。
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: task-close
3
+ version: "1.0.0"
3
4
  description: 任务收尾技能。当 Agent 完成接受的任务后自触发,执行任务验收、总结、文档归档和问题记录。触发条件:Agent 已实际完成任务,需要进行收尾工作时使用。
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: article-fetcher
3
+ version: "1.0.0"
3
4
  description: "Use for the fetch-URL → translate → save-to-Obsidian workflow. Trigger whenever a user has a web URL (article, blog post, tweet, newsletter) and wants it fetched, translated into Chinese, and saved locally — even if they say it obliquely (e.g. \"存到 obsidian\", \"抓取\", \"save this\", \"translate and archive\"). Handles single URLs and batch lists, and X.com / Twitter (Playwright + Chrome Profile). Skip only if: no URL is present (user pasting raw text to translate), user wants a summary without archiving, or user is asking about a site's tech stack without wanting to save anything."
4
5
  ---
5
6
 
@@ -0,0 +1 @@
1
+ { "name": "p-launch", "version": "1.0.0" }