agentskillscanner 0.1.2 → 0.2.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/README.md +76 -28
- package/dist/index.js +485 -64
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -6,14 +6,24 @@
|
|
|
6
6
|
|
|
7
7
|
## English
|
|
8
8
|
|
|
9
|
-
Scan and report all available skills for AI coding assistants
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
Scan and report all available skills for AI coding assistants — supports **Claude Code**, **OpenAI Codex CLI**, **Gemini CLI**, and **GitHub Copilot CLI**.
|
|
10
|
+
|
|
11
|
+
### Supported Tools & Scan Paths
|
|
12
|
+
|
|
13
|
+
| Tool | Level | Path |
|
|
14
|
+
|------|-------|------|
|
|
15
|
+
| Claude Code | User | `~/.claude/skills/*/SKILL.md` |
|
|
16
|
+
| Claude Code | Project | `<project>/.claude/skills/*/SKILL.md` |
|
|
17
|
+
| Claude Code | Plugin | `installed_plugins.json` + each plugin directory |
|
|
18
|
+
| Claude Code | Enterprise | `/Library/Application Support/ClaudeCode/` |
|
|
19
|
+
| Codex CLI | User | `~/.codex/skills/*/SKILL.md` |
|
|
20
|
+
| Codex CLI | Project | `<repoRoot>/.agents/skills/*/SKILL.md` |
|
|
21
|
+
| Codex CLI | Enterprise | `/etc/codex/skills/*/SKILL.md` |
|
|
22
|
+
| Gemini CLI | User | `~/.gemini/skills/*/SKILL.md` |
|
|
23
|
+
| Gemini CLI | Project | `<project>/.gemini/skills/*/SKILL.md` |
|
|
24
|
+
| Gemini CLI | Plugin | `~/.gemini/extensions/*/gemini-extension.json` |
|
|
25
|
+
| Copilot CLI | User | `~/.copilot/mcp-config.json` (MCP servers) |
|
|
26
|
+
| Copilot CLI | Project | `<project>/.github/copilot-instructions.md` |
|
|
17
27
|
|
|
18
28
|
### Installation & Usage
|
|
19
29
|
|
|
@@ -32,6 +42,7 @@ agentskillscanner
|
|
|
32
42
|
-j, --json Output in JSON format
|
|
33
43
|
-d, --project-dir DIR Project directory (default: current working directory)
|
|
34
44
|
-l, --level LEVELS Filter levels (comma-separated: user,project,plugin,enterprise)
|
|
45
|
+
-t, --tool TOOLS Filter tools (comma-separated: claude-code,codex,gemini,copilot)
|
|
35
46
|
-v, --verbose Show full descriptions and paths
|
|
36
47
|
-h, --help Show help
|
|
37
48
|
```
|
|
@@ -39,12 +50,24 @@ agentskillscanner
|
|
|
39
50
|
### Examples
|
|
40
51
|
|
|
41
52
|
```bash
|
|
42
|
-
# Scan all
|
|
53
|
+
# Scan all tools (default)
|
|
43
54
|
agentskillscanner
|
|
44
55
|
|
|
45
|
-
#
|
|
56
|
+
# Only scan Claude Code
|
|
57
|
+
agentskillscanner --tool claude-code
|
|
58
|
+
|
|
59
|
+
# Only scan Codex CLI
|
|
60
|
+
agentskillscanner --tool codex
|
|
61
|
+
|
|
62
|
+
# Scan multiple tools
|
|
63
|
+
agentskillscanner --tool codex,gemini
|
|
64
|
+
|
|
65
|
+
# JSON output (includes tool field)
|
|
46
66
|
agentskillscanner --json
|
|
47
67
|
|
|
68
|
+
# Combine tool and level filters
|
|
69
|
+
agentskillscanner --level user --tool codex
|
|
70
|
+
|
|
48
71
|
# Only user and project levels
|
|
49
72
|
agentskillscanner --level user,project
|
|
50
73
|
|
|
@@ -58,11 +81,11 @@ All scan levels (User, Project, Plugin, Enterprise) are supported on macOS, Linu
|
|
|
58
81
|
|
|
59
82
|
The Enterprise-level scan directory varies by OS:
|
|
60
83
|
|
|
61
|
-
| OS | Enterprise
|
|
62
|
-
| ------- | ----------------------------------------- |
|
|
63
|
-
| macOS | `/Library/Application Support/ClaudeCode` |
|
|
64
|
-
| Linux | `/etc/claude-code` |
|
|
65
|
-
| Windows | `C:\ProgramData\ClaudeCode` |
|
|
84
|
+
| OS | Claude Code Enterprise Dir | Codex CLI Enterprise Dir |
|
|
85
|
+
| ------- | ----------------------------------------- | ------------------------ |
|
|
86
|
+
| macOS | `/Library/Application Support/ClaudeCode` | `/etc/codex/skills` |
|
|
87
|
+
| Linux | `/etc/claude-code` | `/etc/codex/skills` |
|
|
88
|
+
| Windows | `C:\ProgramData\ClaudeCode` | — |
|
|
66
89
|
|
|
67
90
|
### Development
|
|
68
91
|
|
|
@@ -76,12 +99,24 @@ node dist/index.js
|
|
|
76
99
|
|
|
77
100
|
## 繁體中文
|
|
78
101
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
102
|
+
掃描並彙整所有 AI 編碼工具的技能配置 — 支援 **Claude Code**、**OpenAI Codex CLI**、**Gemini CLI** 與 **GitHub Copilot CLI**。
|
|
103
|
+
|
|
104
|
+
### 支援工具與掃描路徑
|
|
105
|
+
|
|
106
|
+
| 工具 | 層級 | 路徑 |
|
|
107
|
+
|------|------|------|
|
|
108
|
+
| Claude Code | 使用者 | `~/.claude/skills/*/SKILL.md` |
|
|
109
|
+
| Claude Code | 專案 | `<project>/.claude/skills/*/SKILL.md` |
|
|
110
|
+
| Claude Code | 外掛 | `installed_plugins.json` + 各外掛目錄 |
|
|
111
|
+
| Claude Code | 企業 | `/Library/Application Support/ClaudeCode/` |
|
|
112
|
+
| Codex CLI | 使用者 | `~/.codex/skills/*/SKILL.md` |
|
|
113
|
+
| Codex CLI | 專案 | `<repoRoot>/.agents/skills/*/SKILL.md` |
|
|
114
|
+
| Codex CLI | 企業 | `/etc/codex/skills/*/SKILL.md` |
|
|
115
|
+
| Gemini CLI | 使用者 | `~/.gemini/skills/*/SKILL.md` |
|
|
116
|
+
| Gemini CLI | 專案 | `<project>/.gemini/skills/*/SKILL.md` |
|
|
117
|
+
| Gemini CLI | 外掛 | `~/.gemini/extensions/*/gemini-extension.json` |
|
|
118
|
+
| Copilot CLI | 使用者 | `~/.copilot/mcp-config.json`(MCP 伺服器) |
|
|
119
|
+
| Copilot CLI | 專案 | `<project>/.github/copilot-instructions.md` |
|
|
85
120
|
|
|
86
121
|
### 安裝與使用
|
|
87
122
|
|
|
@@ -100,6 +135,7 @@ agentskillscanner
|
|
|
100
135
|
-j, --json 以 JSON 格式輸出
|
|
101
136
|
-d, --project-dir DIR 專案目錄(預設:目前工作目錄)
|
|
102
137
|
-l, --level LEVELS 篩選層級(逗號分隔:user,project,plugin,enterprise)
|
|
138
|
+
-t, --tool TOOLS 篩選工具(逗號分隔:claude-code,codex,gemini,copilot)
|
|
103
139
|
-v, --verbose 顯示完整描述與路徑
|
|
104
140
|
-h, --help 顯示說明
|
|
105
141
|
```
|
|
@@ -107,12 +143,24 @@ agentskillscanner
|
|
|
107
143
|
### 範例
|
|
108
144
|
|
|
109
145
|
```bash
|
|
110
|
-
#
|
|
146
|
+
# 預設掃描所有工具
|
|
111
147
|
agentskillscanner
|
|
112
148
|
|
|
113
|
-
#
|
|
149
|
+
# 只掃描 Claude Code
|
|
150
|
+
agentskillscanner --tool claude-code
|
|
151
|
+
|
|
152
|
+
# 只掃描 Codex CLI
|
|
153
|
+
agentskillscanner --tool codex
|
|
154
|
+
|
|
155
|
+
# 掃描多個工具
|
|
156
|
+
agentskillscanner --tool codex,gemini
|
|
157
|
+
|
|
158
|
+
# JSON 輸出(含 tool 欄位)
|
|
114
159
|
agentskillscanner --json
|
|
115
160
|
|
|
161
|
+
# 組合工具與層級篩選
|
|
162
|
+
agentskillscanner --level user --tool codex
|
|
163
|
+
|
|
116
164
|
# 只看使用者與專案層級
|
|
117
165
|
agentskillscanner --level user,project
|
|
118
166
|
|
|
@@ -126,11 +174,11 @@ agentskillscanner --verbose
|
|
|
126
174
|
|
|
127
175
|
Enterprise 層級的掃描目錄依作業系統不同:
|
|
128
176
|
|
|
129
|
-
| 作業系統 | Enterprise 目錄
|
|
130
|
-
| -------- | ----------------------------------------- |
|
|
131
|
-
| macOS | `/Library/Application Support/ClaudeCode` |
|
|
132
|
-
| Linux | `/etc/claude-code` |
|
|
133
|
-
| Windows | `C:\ProgramData\ClaudeCode` |
|
|
177
|
+
| 作業系統 | Claude Code Enterprise 目錄 | Codex CLI Enterprise 目錄 |
|
|
178
|
+
| -------- | ----------------------------------------- | ------------------------- |
|
|
179
|
+
| macOS | `/Library/Application Support/ClaudeCode` | `/etc/codex/skills` |
|
|
180
|
+
| Linux | `/etc/claude-code` | `/etc/codex/skills` |
|
|
181
|
+
| Windows | `C:\ProgramData\ClaudeCode` | — |
|
|
134
182
|
|
|
135
183
|
### 開發
|
|
136
184
|
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { cli, define } from "gunshi";
|
|
3
|
-
import
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
4
|
+
import updateNotifier from "update-notifier";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
6
|
import { homedir, platform } from "node:os";
|
|
7
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
6
8
|
import pc from "picocolors";
|
|
7
9
|
|
|
10
|
+
//#region src/types.ts
|
|
11
|
+
const Tool = {
|
|
12
|
+
CLAUDE_CODE: "claude-code",
|
|
13
|
+
CODEX: "codex",
|
|
14
|
+
GEMINI: "gemini",
|
|
15
|
+
COPILOT: "copilot"
|
|
16
|
+
};
|
|
17
|
+
const TOOL_LABELS = {
|
|
18
|
+
[Tool.CLAUDE_CODE]: "Claude Code",
|
|
19
|
+
[Tool.CODEX]: "OpenAI Codex CLI",
|
|
20
|
+
[Tool.GEMINI]: "Gemini CLI",
|
|
21
|
+
[Tool.COPILOT]: "GitHub Copilot CLI"
|
|
22
|
+
};
|
|
23
|
+
const SkillLevel = {
|
|
24
|
+
USER: "user",
|
|
25
|
+
PROJECT: "project",
|
|
26
|
+
PLUGIN: "plugin",
|
|
27
|
+
ENTERPRISE: "enterprise"
|
|
28
|
+
};
|
|
29
|
+
const SkillType = {
|
|
30
|
+
SKILL: "skill",
|
|
31
|
+
COMMAND: "command",
|
|
32
|
+
AGENT: "agent",
|
|
33
|
+
HOOK: "hook"
|
|
34
|
+
};
|
|
35
|
+
const LEVEL_LABELS = {
|
|
36
|
+
[SkillLevel.USER]: "使用者層級 (User)",
|
|
37
|
+
[SkillLevel.PROJECT]: "專案層級 (Project)",
|
|
38
|
+
[SkillLevel.PLUGIN]: "外掛層級 (Plugin)",
|
|
39
|
+
[SkillLevel.ENTERPRISE]: "企業層級 (Enterprise)"
|
|
40
|
+
};
|
|
41
|
+
const TYPE_LABELS = {
|
|
42
|
+
[SkillType.SKILL]: "技能",
|
|
43
|
+
[SkillType.COMMAND]: "命令",
|
|
44
|
+
[SkillType.AGENT]: "代理",
|
|
45
|
+
[SkillType.HOOK]: "鉤子"
|
|
46
|
+
};
|
|
47
|
+
function byLevel(result, level) {
|
|
48
|
+
return result.skills.filter((s) => s.level === level);
|
|
49
|
+
}
|
|
50
|
+
function byTool(result, tool) {
|
|
51
|
+
return {
|
|
52
|
+
skills: result.skills.filter((s) => s.tool === tool),
|
|
53
|
+
plugins: result.plugins.filter((p) => p.tool === tool)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
8
58
|
//#region src/frontmatter.ts
|
|
9
59
|
function parseFrontmatter(text) {
|
|
10
60
|
const match = text.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
@@ -42,38 +92,40 @@ function parseFrontmatter(text) {
|
|
|
42
92
|
}
|
|
43
93
|
|
|
44
94
|
//#endregion
|
|
45
|
-
//#region src/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
95
|
+
//#region src/fs-utils.ts
|
|
96
|
+
function isDir(p) {
|
|
97
|
+
try {
|
|
98
|
+
return statSync(p).isDirectory();
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function isFile(p) {
|
|
104
|
+
try {
|
|
105
|
+
return statSync(p).isFile();
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function sortedDir(p) {
|
|
111
|
+
try {
|
|
112
|
+
return readdirSync(p).sort();
|
|
113
|
+
} catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function readFileSafe(p) {
|
|
118
|
+
try {
|
|
119
|
+
return readFileSync(p, "utf-8");
|
|
120
|
+
} catch {
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
72
123
|
}
|
|
73
124
|
|
|
74
125
|
//#endregion
|
|
75
|
-
//#region src/
|
|
76
|
-
var
|
|
126
|
+
//#region src/scanners/claude-code.ts
|
|
127
|
+
var ClaudeCodeScanner = class {
|
|
128
|
+
tool = Tool.CLAUDE_CODE;
|
|
77
129
|
projectDir;
|
|
78
130
|
home;
|
|
79
131
|
claudeDir;
|
|
@@ -126,6 +178,7 @@ var Scanner = class {
|
|
|
126
178
|
delete fm.description;
|
|
127
179
|
items.push({
|
|
128
180
|
name,
|
|
181
|
+
tool: this.tool,
|
|
129
182
|
skillType: SkillType.SKILL,
|
|
130
183
|
level,
|
|
131
184
|
description: desc,
|
|
@@ -175,6 +228,7 @@ var Scanner = class {
|
|
|
175
228
|
} catch {}
|
|
176
229
|
const pinfo = {
|
|
177
230
|
name: pluginName,
|
|
231
|
+
tool: this.tool,
|
|
178
232
|
marketplace,
|
|
179
233
|
installPath,
|
|
180
234
|
version: ver,
|
|
@@ -191,6 +245,7 @@ var Scanner = class {
|
|
|
191
245
|
makePluginSkill(name, stype, path, pinfo, description = "", extra = {}) {
|
|
192
246
|
return {
|
|
193
247
|
name,
|
|
248
|
+
tool: this.tool,
|
|
194
249
|
skillType: stype,
|
|
195
250
|
level: SkillLevel.PLUGIN,
|
|
196
251
|
description,
|
|
@@ -294,6 +349,7 @@ var Scanner = class {
|
|
|
294
349
|
delete fm.description;
|
|
295
350
|
items.push({
|
|
296
351
|
name,
|
|
352
|
+
tool: this.tool,
|
|
297
353
|
skillType: SkillType.SKILL,
|
|
298
354
|
level: SkillLevel.ENTERPRISE,
|
|
299
355
|
description: desc,
|
|
@@ -307,35 +363,330 @@ var Scanner = class {
|
|
|
307
363
|
return items;
|
|
308
364
|
}
|
|
309
365
|
};
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
366
|
+
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region src/scanners/codex.ts
|
|
369
|
+
var CodexScanner = class {
|
|
370
|
+
tool = Tool.CODEX;
|
|
371
|
+
projectDir;
|
|
372
|
+
home;
|
|
373
|
+
constructor(projectDir) {
|
|
374
|
+
this.projectDir = resolve(projectDir);
|
|
375
|
+
this.home = homedir();
|
|
315
376
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
377
|
+
scan(levels) {
|
|
378
|
+
const targets = levels ?? [
|
|
379
|
+
SkillLevel.USER,
|
|
380
|
+
SkillLevel.PROJECT,
|
|
381
|
+
SkillLevel.PLUGIN,
|
|
382
|
+
SkillLevel.ENTERPRISE
|
|
383
|
+
];
|
|
384
|
+
const result = {
|
|
385
|
+
skills: [],
|
|
386
|
+
plugins: []
|
|
387
|
+
};
|
|
388
|
+
if (targets.includes(SkillLevel.USER)) result.skills.push(...this.scanUser());
|
|
389
|
+
if (targets.includes(SkillLevel.PROJECT)) result.skills.push(...this.scanProject());
|
|
390
|
+
if (targets.includes(SkillLevel.ENTERPRISE)) result.skills.push(...this.scanEnterprise());
|
|
391
|
+
return result;
|
|
322
392
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
393
|
+
scanUser() {
|
|
394
|
+
const skillsDir = join(this.home, ".codex", "skills");
|
|
395
|
+
if (!isDir(skillsDir)) return [];
|
|
396
|
+
const items = [];
|
|
397
|
+
for (const child of sortedDir(skillsDir)) {
|
|
398
|
+
if (child === ".system") continue;
|
|
399
|
+
const childPath = join(skillsDir, child);
|
|
400
|
+
if (!isDir(childPath)) continue;
|
|
401
|
+
const skill = this.readSkillMd(childPath, child, SkillLevel.USER);
|
|
402
|
+
if (skill) items.push(skill);
|
|
403
|
+
}
|
|
404
|
+
const systemDir = join(skillsDir, ".system");
|
|
405
|
+
if (isDir(systemDir)) for (const child of sortedDir(systemDir)) {
|
|
406
|
+
const childPath = join(systemDir, child);
|
|
407
|
+
if (!isDir(childPath)) continue;
|
|
408
|
+
const skill = this.readSkillMd(childPath, child, SkillLevel.USER);
|
|
409
|
+
if (skill) {
|
|
410
|
+
skill.extra = {
|
|
411
|
+
...skill.extra,
|
|
412
|
+
bundled: "true"
|
|
413
|
+
};
|
|
414
|
+
items.push(skill);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return items;
|
|
329
418
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
419
|
+
scanProject() {
|
|
420
|
+
const repoRoot = findRepoRoot(this.projectDir);
|
|
421
|
+
if (!repoRoot) return [];
|
|
422
|
+
const skillsDir = join(repoRoot, ".agents", "skills");
|
|
423
|
+
return this.scanSkillDir(skillsDir, SkillLevel.PROJECT);
|
|
424
|
+
}
|
|
425
|
+
scanEnterprise() {
|
|
426
|
+
return this.scanSkillDir("/etc/codex/skills", SkillLevel.ENTERPRISE);
|
|
427
|
+
}
|
|
428
|
+
scanSkillDir(skillsDir, level) {
|
|
429
|
+
if (!isDir(skillsDir)) return [];
|
|
430
|
+
const items = [];
|
|
431
|
+
for (const child of sortedDir(skillsDir)) {
|
|
432
|
+
const childPath = join(skillsDir, child);
|
|
433
|
+
if (!isDir(childPath)) continue;
|
|
434
|
+
const skill = this.readSkillMd(childPath, child, level);
|
|
435
|
+
if (skill) items.push(skill);
|
|
436
|
+
}
|
|
437
|
+
return items;
|
|
438
|
+
}
|
|
439
|
+
readSkillMd(dir, fallbackName, level) {
|
|
440
|
+
const skillMd = join(dir, "SKILL.md");
|
|
441
|
+
if (!isFile(skillMd)) return null;
|
|
442
|
+
const fm = parseFrontmatter(readFileSafe(skillMd));
|
|
443
|
+
const name = fm.name ?? fallbackName;
|
|
444
|
+
const desc = fm.description ?? "";
|
|
445
|
+
delete fm.name;
|
|
446
|
+
delete fm.description;
|
|
447
|
+
return {
|
|
448
|
+
name,
|
|
449
|
+
tool: this.tool,
|
|
450
|
+
skillType: SkillType.SKILL,
|
|
451
|
+
level,
|
|
452
|
+
description: desc,
|
|
453
|
+
path: skillMd,
|
|
454
|
+
pluginName: "",
|
|
455
|
+
marketplace: "",
|
|
456
|
+
enabled: true,
|
|
457
|
+
extra: fm
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
function findRepoRoot(startDir) {
|
|
462
|
+
let dir = resolve(startDir);
|
|
463
|
+
const root = dirname(dir) === dir ? dir : void 0;
|
|
464
|
+
while (true) {
|
|
465
|
+
if (isDir(join(dir, ".git"))) return dir;
|
|
466
|
+
const parent = dirname(dir);
|
|
467
|
+
if (parent === dir || parent === root) return null;
|
|
468
|
+
dir = parent;
|
|
336
469
|
}
|
|
337
470
|
}
|
|
338
471
|
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region src/scanners/gemini.ts
|
|
474
|
+
var GeminiScanner = class {
|
|
475
|
+
tool = Tool.GEMINI;
|
|
476
|
+
projectDir;
|
|
477
|
+
home;
|
|
478
|
+
constructor(projectDir) {
|
|
479
|
+
this.projectDir = resolve(projectDir);
|
|
480
|
+
this.home = homedir();
|
|
481
|
+
}
|
|
482
|
+
scan(levels) {
|
|
483
|
+
const targets = levels ?? [
|
|
484
|
+
SkillLevel.USER,
|
|
485
|
+
SkillLevel.PROJECT,
|
|
486
|
+
SkillLevel.PLUGIN,
|
|
487
|
+
SkillLevel.ENTERPRISE
|
|
488
|
+
];
|
|
489
|
+
const result = {
|
|
490
|
+
skills: [],
|
|
491
|
+
plugins: []
|
|
492
|
+
};
|
|
493
|
+
if (targets.includes(SkillLevel.USER)) result.skills.push(...this.scanUser());
|
|
494
|
+
if (targets.includes(SkillLevel.PROJECT)) result.skills.push(...this.scanProject());
|
|
495
|
+
if (targets.includes(SkillLevel.PLUGIN)) {
|
|
496
|
+
const { skills, plugins } = this.scanExtensions();
|
|
497
|
+
result.skills.push(...skills);
|
|
498
|
+
result.plugins.push(...plugins);
|
|
499
|
+
}
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
scanUser() {
|
|
503
|
+
const skillsDir = join(this.home, ".gemini", "skills");
|
|
504
|
+
return this.scanSkillDir(skillsDir, SkillLevel.USER);
|
|
505
|
+
}
|
|
506
|
+
scanProject() {
|
|
507
|
+
const skillsDir = join(this.projectDir, ".gemini", "skills");
|
|
508
|
+
return this.scanSkillDir(skillsDir, SkillLevel.PROJECT);
|
|
509
|
+
}
|
|
510
|
+
scanSkillDir(skillsDir, level) {
|
|
511
|
+
if (!isDir(skillsDir)) return [];
|
|
512
|
+
const items = [];
|
|
513
|
+
for (const child of sortedDir(skillsDir)) {
|
|
514
|
+
const childPath = join(skillsDir, child);
|
|
515
|
+
if (!isDir(childPath)) continue;
|
|
516
|
+
const skillMd = join(childPath, "SKILL.md");
|
|
517
|
+
if (!isFile(skillMd)) continue;
|
|
518
|
+
const fm = parseFrontmatter(readFileSafe(skillMd));
|
|
519
|
+
const name = fm.name ?? child;
|
|
520
|
+
const desc = fm.description ?? "";
|
|
521
|
+
delete fm.name;
|
|
522
|
+
delete fm.description;
|
|
523
|
+
items.push({
|
|
524
|
+
name,
|
|
525
|
+
tool: this.tool,
|
|
526
|
+
skillType: SkillType.SKILL,
|
|
527
|
+
level,
|
|
528
|
+
description: desc,
|
|
529
|
+
path: skillMd,
|
|
530
|
+
pluginName: "",
|
|
531
|
+
marketplace: "",
|
|
532
|
+
enabled: true,
|
|
533
|
+
extra: fm
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
return items;
|
|
537
|
+
}
|
|
538
|
+
scanExtensions() {
|
|
539
|
+
const skills = [];
|
|
540
|
+
const plugins = [];
|
|
541
|
+
const dirs = [join(this.home, ".gemini", "extensions"), join(this.projectDir, ".gemini", "extensions")];
|
|
542
|
+
for (const extDir of dirs) {
|
|
543
|
+
if (!isDir(extDir)) continue;
|
|
544
|
+
for (const child of sortedDir(extDir)) {
|
|
545
|
+
const childPath = join(extDir, child);
|
|
546
|
+
if (!isDir(childPath)) continue;
|
|
547
|
+
const extJson = join(childPath, "gemini-extension.json");
|
|
548
|
+
if (!isFile(extJson)) continue;
|
|
549
|
+
let data;
|
|
550
|
+
try {
|
|
551
|
+
data = JSON.parse(readFileSafe(extJson));
|
|
552
|
+
} catch {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const name = data.name ?? child;
|
|
556
|
+
const desc = data.description ?? "";
|
|
557
|
+
const pinfo = {
|
|
558
|
+
name,
|
|
559
|
+
tool: this.tool,
|
|
560
|
+
marketplace: "",
|
|
561
|
+
installPath: childPath,
|
|
562
|
+
version: "",
|
|
563
|
+
enabled: true,
|
|
564
|
+
description: desc,
|
|
565
|
+
author: "",
|
|
566
|
+
items: []
|
|
567
|
+
};
|
|
568
|
+
plugins.push(pinfo);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
skills,
|
|
573
|
+
plugins
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region src/scanners/copilot.ts
|
|
580
|
+
var CopilotScanner = class {
|
|
581
|
+
tool = Tool.COPILOT;
|
|
582
|
+
projectDir;
|
|
583
|
+
home;
|
|
584
|
+
constructor(projectDir) {
|
|
585
|
+
this.projectDir = resolve(projectDir);
|
|
586
|
+
this.home = homedir();
|
|
587
|
+
}
|
|
588
|
+
scan(levels) {
|
|
589
|
+
const targets = levels ?? [
|
|
590
|
+
SkillLevel.USER,
|
|
591
|
+
SkillLevel.PROJECT,
|
|
592
|
+
SkillLevel.PLUGIN,
|
|
593
|
+
SkillLevel.ENTERPRISE
|
|
594
|
+
];
|
|
595
|
+
const result = {
|
|
596
|
+
skills: [],
|
|
597
|
+
plugins: []
|
|
598
|
+
};
|
|
599
|
+
if (targets.includes(SkillLevel.USER)) result.skills.push(...this.scanUser());
|
|
600
|
+
if (targets.includes(SkillLevel.PROJECT)) result.skills.push(...this.scanProject());
|
|
601
|
+
return result;
|
|
602
|
+
}
|
|
603
|
+
scanUser() {
|
|
604
|
+
const mcpConfig = join(this.home, ".copilot", "mcp-config.json");
|
|
605
|
+
if (!isFile(mcpConfig)) return [];
|
|
606
|
+
let data;
|
|
607
|
+
try {
|
|
608
|
+
data = JSON.parse(readFileSafe(mcpConfig));
|
|
609
|
+
} catch {
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
const servers = data.mcpServers ?? data.servers ?? {};
|
|
613
|
+
const items = [];
|
|
614
|
+
for (const serverName of Object.keys(servers).sort()) {
|
|
615
|
+
const desc = servers[serverName]?.description ?? "";
|
|
616
|
+
items.push({
|
|
617
|
+
name: serverName,
|
|
618
|
+
tool: this.tool,
|
|
619
|
+
skillType: SkillType.COMMAND,
|
|
620
|
+
level: SkillLevel.USER,
|
|
621
|
+
description: desc,
|
|
622
|
+
path: mcpConfig,
|
|
623
|
+
pluginName: "",
|
|
624
|
+
marketplace: "",
|
|
625
|
+
enabled: true,
|
|
626
|
+
extra: { source: "mcp-config" }
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
return items;
|
|
630
|
+
}
|
|
631
|
+
scanProject() {
|
|
632
|
+
const instrFile = join(this.projectDir, ".github", "copilot-instructions.md");
|
|
633
|
+
if (!isFile(instrFile)) return [];
|
|
634
|
+
const firstLine = readFileSafe(instrFile).split("\n").find((l) => l.trim().length > 0)?.trim() ?? "";
|
|
635
|
+
const desc = firstLine.startsWith("#") ? firstLine.replace(/^#+\s*/, "") : firstLine.slice(0, 100);
|
|
636
|
+
return [{
|
|
637
|
+
name: "copilot-instructions",
|
|
638
|
+
tool: this.tool,
|
|
639
|
+
skillType: SkillType.SKILL,
|
|
640
|
+
level: SkillLevel.PROJECT,
|
|
641
|
+
description: desc || "Copilot project instructions",
|
|
642
|
+
path: instrFile,
|
|
643
|
+
pluginName: "",
|
|
644
|
+
marketplace: "",
|
|
645
|
+
enabled: true,
|
|
646
|
+
extra: {}
|
|
647
|
+
}];
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
//#endregion
|
|
652
|
+
//#region src/multi-scanner.ts
|
|
653
|
+
const ALL_TOOLS = [
|
|
654
|
+
Tool.CLAUDE_CODE,
|
|
655
|
+
Tool.CODEX,
|
|
656
|
+
Tool.GEMINI,
|
|
657
|
+
Tool.COPILOT
|
|
658
|
+
];
|
|
659
|
+
var MultiScanner = class {
|
|
660
|
+
projectDir;
|
|
661
|
+
constructor(projectDir) {
|
|
662
|
+
this.projectDir = resolve(projectDir);
|
|
663
|
+
}
|
|
664
|
+
scan(tools, levels) {
|
|
665
|
+
const targetTools = tools ?? ALL_TOOLS;
|
|
666
|
+
const result = {
|
|
667
|
+
skills: [],
|
|
668
|
+
plugins: []
|
|
669
|
+
};
|
|
670
|
+
for (const tool of targetTools) {
|
|
671
|
+
const scanner = this.createScanner(tool);
|
|
672
|
+
if (!scanner) continue;
|
|
673
|
+
const partial = scanner.scan(levels);
|
|
674
|
+
result.skills.push(...partial.skills);
|
|
675
|
+
result.plugins.push(...partial.plugins);
|
|
676
|
+
}
|
|
677
|
+
return result;
|
|
678
|
+
}
|
|
679
|
+
createScanner(tool) {
|
|
680
|
+
switch (tool) {
|
|
681
|
+
case Tool.CLAUDE_CODE: return new ClaudeCodeScanner(this.projectDir);
|
|
682
|
+
case Tool.CODEX: return new CodexScanner(this.projectDir);
|
|
683
|
+
case Tool.GEMINI: return new GeminiScanner(this.projectDir);
|
|
684
|
+
case Tool.COPILOT: return new CopilotScanner(this.projectDir);
|
|
685
|
+
default: return null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
339
690
|
//#endregion
|
|
340
691
|
//#region src/formatter.ts
|
|
341
692
|
function truncate(text, maxLen = 72) {
|
|
@@ -343,14 +694,13 @@ function truncate(text, maxLen = 72) {
|
|
|
343
694
|
if (clean.length <= maxLen) return clean;
|
|
344
695
|
return clean.slice(0, maxLen - 3) + "...";
|
|
345
696
|
}
|
|
346
|
-
function
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
lines.push("");
|
|
697
|
+
function getDistinctTools(result) {
|
|
698
|
+
const tools = /* @__PURE__ */ new Set();
|
|
699
|
+
for (const s of result.skills) tools.add(s.tool);
|
|
700
|
+
for (const p of result.plugins) tools.add(p.tool);
|
|
701
|
+
return [...tools];
|
|
702
|
+
}
|
|
703
|
+
function renderLevelBlock(result, verbose, lines) {
|
|
354
704
|
let anyOutput = false;
|
|
355
705
|
for (const level of [
|
|
356
706
|
SkillLevel.USER,
|
|
@@ -403,11 +753,52 @@ function formatTerminal(result, verbose) {
|
|
|
403
753
|
lines.push("");
|
|
404
754
|
}
|
|
405
755
|
}
|
|
756
|
+
return anyOutput;
|
|
757
|
+
}
|
|
758
|
+
function formatTerminal(result, verbose) {
|
|
759
|
+
const lines = [];
|
|
760
|
+
const tools = getDistinctTools(result);
|
|
761
|
+
const multiTool = tools.length > 1;
|
|
762
|
+
let title;
|
|
763
|
+
if (multiTool) title = "AI Coding Tools 技能掃描報告";
|
|
764
|
+
else if (tools.length === 1) title = `${TOOL_LABELS[tools[0]]} 技能掃描報告`;
|
|
765
|
+
else title = "AI Coding Tools 技能掃描報告";
|
|
766
|
+
const boxW = 58;
|
|
767
|
+
lines.push(pc.cyan("╔" + "═".repeat(boxW) + "╗"));
|
|
768
|
+
lines.push(pc.cyan("║") + pc.bold(pc.white(" " + title.padEnd(boxW - 2))) + pc.cyan("║"));
|
|
769
|
+
lines.push(pc.cyan("╚" + "═".repeat(boxW) + "╝"));
|
|
770
|
+
lines.push("");
|
|
771
|
+
let anyOutput = false;
|
|
772
|
+
if (multiTool) for (const tool of tools) {
|
|
773
|
+
const toolResult = byTool(result, tool);
|
|
774
|
+
const toolLabel = TOOL_LABELS[tool];
|
|
775
|
+
lines.push(pc.bold(pc.cyan(`▶ ${toolLabel}`)));
|
|
776
|
+
lines.push("");
|
|
777
|
+
const hadOutput = renderLevelBlock(toolResult, verbose, lines);
|
|
778
|
+
if (hadOutput) anyOutput = true;
|
|
779
|
+
if (!hadOutput) {
|
|
780
|
+
lines.push(` ${pc.dim("(未掃描到任何技能)")}`);
|
|
781
|
+
lines.push("");
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
else anyOutput = renderLevelBlock(result, verbose, lines);
|
|
406
785
|
if (!anyOutput) {
|
|
407
786
|
lines.push(` ${pc.dim("(未掃描到任何技能)")}`);
|
|
408
787
|
lines.push("");
|
|
409
788
|
}
|
|
410
789
|
lines.push(pc.bold(`── 統計摘要 ${"─".repeat(47)}`));
|
|
790
|
+
if (multiTool) {
|
|
791
|
+
for (const tool of tools) {
|
|
792
|
+
const toolResult = byTool(result, tool);
|
|
793
|
+
const toolLabel = TOOL_LABELS[tool];
|
|
794
|
+
const count = toolResult.skills.length;
|
|
795
|
+
const pluginCount = toolResult.plugins.length;
|
|
796
|
+
let detail = `${count} 個項目`;
|
|
797
|
+
if (pluginCount > 0) detail += `, ${pluginCount} 個外掛`;
|
|
798
|
+
lines.push(` ${pc.bold(toolLabel)}: ${detail}`);
|
|
799
|
+
}
|
|
800
|
+
lines.push("");
|
|
801
|
+
}
|
|
411
802
|
const userCnt = byLevel(result, SkillLevel.USER).length;
|
|
412
803
|
const projCnt = byLevel(result, SkillLevel.PROJECT).length;
|
|
413
804
|
const entCnt = byLevel(result, SkillLevel.ENTERPRISE).length;
|
|
@@ -427,9 +818,11 @@ function formatTerminal(result, verbose) {
|
|
|
427
818
|
return lines.join("\n");
|
|
428
819
|
}
|
|
429
820
|
function formatJson(result) {
|
|
821
|
+
const tools = getDistinctTools(result);
|
|
430
822
|
const output = {
|
|
431
823
|
skills: result.skills.map((s) => ({
|
|
432
824
|
name: s.name,
|
|
825
|
+
tool: s.tool,
|
|
433
826
|
skill_type: s.skillType,
|
|
434
827
|
level: s.level,
|
|
435
828
|
description: s.description,
|
|
@@ -441,6 +834,7 @@ function formatJson(result) {
|
|
|
441
834
|
})),
|
|
442
835
|
plugins: result.plugins.map((p) => ({
|
|
443
836
|
name: p.name,
|
|
837
|
+
tool: p.tool,
|
|
444
838
|
marketplace: p.marketplace,
|
|
445
839
|
install_path: p.installPath,
|
|
446
840
|
version: p.version,
|
|
@@ -449,6 +843,7 @@ function formatJson(result) {
|
|
|
449
843
|
author: p.author,
|
|
450
844
|
items: p.items.map((i) => ({
|
|
451
845
|
name: i.name,
|
|
846
|
+
tool: i.tool,
|
|
452
847
|
skill_type: i.skillType,
|
|
453
848
|
level: i.level,
|
|
454
849
|
description: i.description,
|
|
@@ -465,7 +860,14 @@ function formatJson(result) {
|
|
|
465
860
|
enterprise: byLevel(result, SkillLevel.ENTERPRISE).length,
|
|
466
861
|
plugin_count: result.plugins.length,
|
|
467
862
|
plugin_items: byLevel(result, SkillLevel.PLUGIN).length,
|
|
468
|
-
total: result.skills.length
|
|
863
|
+
total: result.skills.length,
|
|
864
|
+
by_tool: Object.fromEntries(tools.map((t) => {
|
|
865
|
+
const tr = byTool(result, t);
|
|
866
|
+
return [t, {
|
|
867
|
+
skills: tr.skills.length,
|
|
868
|
+
plugins: tr.plugins.length
|
|
869
|
+
}];
|
|
870
|
+
}))
|
|
469
871
|
}
|
|
470
872
|
};
|
|
471
873
|
return JSON.stringify(output, null, 2);
|
|
@@ -479,6 +881,13 @@ const LEVEL_MAP = {
|
|
|
479
881
|
plugin: SkillLevel.PLUGIN,
|
|
480
882
|
enterprise: SkillLevel.ENTERPRISE
|
|
481
883
|
};
|
|
884
|
+
const TOOL_MAP = {
|
|
885
|
+
"claude-code": Tool.CLAUDE_CODE,
|
|
886
|
+
claude: Tool.CLAUDE_CODE,
|
|
887
|
+
codex: Tool.CODEX,
|
|
888
|
+
gemini: Tool.GEMINI,
|
|
889
|
+
copilot: Tool.COPILOT
|
|
890
|
+
};
|
|
482
891
|
const scanCommand = define({
|
|
483
892
|
name: "agentskillscanner",
|
|
484
893
|
description: "Scan and report all available skills for AI coding assistants",
|
|
@@ -500,6 +909,11 @@ const scanCommand = define({
|
|
|
500
909
|
short: "l",
|
|
501
910
|
description: "篩選層級:user, project, plugin, enterprise(可用逗號分隔多個)"
|
|
502
911
|
},
|
|
912
|
+
tool: {
|
|
913
|
+
type: "string",
|
|
914
|
+
short: "t",
|
|
915
|
+
description: "篩選工具:claude-code, codex, gemini, copilot(可用逗號分隔多個)"
|
|
916
|
+
},
|
|
503
917
|
verbose: {
|
|
504
918
|
type: "boolean",
|
|
505
919
|
short: "v",
|
|
@@ -516,7 +930,13 @@ const scanCommand = define({
|
|
|
516
930
|
levelFilter = levelArg.split(",").map((s) => s.trim().toLowerCase()).map((v) => LEVEL_MAP[v]).filter((v) => v !== void 0);
|
|
517
931
|
if (levelFilter.length === 0) levelFilter = void 0;
|
|
518
932
|
}
|
|
519
|
-
|
|
933
|
+
let toolFilter;
|
|
934
|
+
const toolArg = ctx.values.tool;
|
|
935
|
+
if (toolArg) {
|
|
936
|
+
toolFilter = toolArg.split(",").map((s) => s.trim().toLowerCase()).map((v) => TOOL_MAP[v]).filter((v) => v !== void 0);
|
|
937
|
+
if (toolFilter.length === 0) toolFilter = void 0;
|
|
938
|
+
}
|
|
939
|
+
const result = new MultiScanner(projectDir).scan(toolFilter, levelFilter);
|
|
520
940
|
if (json) console.log(formatJson(result));
|
|
521
941
|
else console.log(formatTerminal(result, verbose ?? false));
|
|
522
942
|
}
|
|
@@ -524,6 +944,7 @@ const scanCommand = define({
|
|
|
524
944
|
|
|
525
945
|
//#endregion
|
|
526
946
|
//#region src/index.ts
|
|
947
|
+
updateNotifier({ pkg: createRequire(import.meta.url)("../package.json") }).notify();
|
|
527
948
|
await cli(process.argv.slice(2), scanCommand);
|
|
528
949
|
|
|
529
950
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentskillscanner",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Scan and report all available skills for AI coding assistants",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"gunshi": "^0.26.3",
|
|
22
|
-
"picocolors": "^1.1.1"
|
|
22
|
+
"picocolors": "^1.1.1",
|
|
23
|
+
"update-notifier": "^7.3.1"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"tsdown": "^0.12.5",
|