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.
Files changed (3) hide show
  1. package/README.md +90 -18
  2. package/dist/index.js +492 -67
  3. 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 (Claude Code).
10
-
11
- Scans across four levels:
12
-
13
- 1. **User** `~/.claude/skills/*/SKILL.md`
14
- 2. **Project** — `<project>/.claude/skills/*/SKILL.md`
15
- 3. **Plugin** `installed_plugins.json` + each plugin directory
16
- 4. **Enterprise** `/Library/Application Support/ClaudeCode/`
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 levels (default)
53
+ # Scan all tools (default)
43
54
  agentskillscanner
44
55
 
45
- # JSON output
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
- 掃描並彙整所有可用的 Claude Code skills,涵蓋四個層級:
68
-
69
- 1. **使用者層級 (User)** — `~/.claude/skills/*/SKILL.md`
70
- 2. **專案層級 (Project)** — `<project>/.claude/skills/*/SKILL.md`
71
- 3. **外掛層級 (Plugin)** `installed_plugins.json` + 各外掛目錄
72
- 4. **企業層級 (Enterprise)** — `/Library/Application Support/ClaudeCode/`
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
- # JSON 輸出
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 { readFileSync, readdirSync, statSync } from "node:fs";
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/types.ts
46
- const SkillLevel = {
47
- USER: "user",
48
- PROJECT: "project",
49
- PLUGIN: "plugin",
50
- ENTERPRISE: "enterprise"
51
- };
52
- const SkillType = {
53
- SKILL: "skill",
54
- COMMAND: "command",
55
- AGENT: "agent",
56
- HOOK: "hook"
57
- };
58
- const LEVEL_LABELS = {
59
- [SkillLevel.USER]: "使用者層級 (User)",
60
- [SkillLevel.PROJECT]: "專案層級 (Project)",
61
- [SkillLevel.PLUGIN]: "外掛層級 (Plugin)",
62
- [SkillLevel.ENTERPRISE]: "企業層級 (Enterprise)"
63
- };
64
- const TYPE_LABELS = {
65
- [SkillType.SKILL]: "技能",
66
- [SkillType.COMMAND]: "命令",
67
- [SkillType.AGENT]: "代理",
68
- [SkillType.HOOK]: "鉤子"
69
- };
70
- function byLevel(result, level) {
71
- return result.skills.filter((s) => s.level === level);
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/scanner.ts
76
- var Scanner = class {
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
- if (platform() !== "darwin") return [];
273
- const entDir = "/Library/Application Support/ClaudeCode";
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
- function isDir(p) {
304
- try {
305
- return statSync(p).isDirectory();
306
- } catch {
307
- return false;
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
- function isFile(p) {
311
- try {
312
- return statSync(p).isFile();
313
- } catch {
314
- return false;
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
- function sortedDir(p) {
318
- try {
319
- return readdirSync(p).sort();
320
- } catch {
321
- return [];
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
- function readFileSafe(p) {
325
- try {
326
- return readFileSync(p, "utf-8");
327
- } catch {
328
- return "";
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 formatTerminal(result, verbose) {
340
- const lines = [];
341
- const title = "Claude Code 技能掃描報告";
342
- const boxW = 58;
343
- lines.push(pc.cyan("╔" + "═".repeat(boxW) + "╗"));
344
- lines.push(pc.cyan("║") + pc.bold(pc.white(" " + title.padEnd(boxW - 2))) + pc.cyan("║"));
345
- lines.push(pc.cyan("╚" + "═".repeat(boxW) + "╝"));
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
- const result = new Scanner(projectDir).scan(levelFilter);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentskillscanner",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Scan and report all available skills for AI coding assistants",
6
6
  "license": "MIT",