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