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.
Files changed (3) hide show
  1. package/README.md +76 -28
  2. package/dist/index.js +485 -64
  3. 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 (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
 
@@ -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 Directory |
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
- 掃描並彙整所有可用的 Claude Code skills,涵蓋四個層級:
80
-
81
- 1. **使用者層級 (User)** — `~/.claude/skills/*/SKILL.md`
82
- 2. **專案層級 (Project)** — `<project>/.claude/skills/*/SKILL.md`
83
- 3. **外掛層級 (Plugin)** `installed_plugins.json` + 各外掛目錄
84
- 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` |
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
- # 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 欄位)
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 { readFileSync, readdirSync, statSync } from "node:fs";
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/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);
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/scanner.ts
76
- var Scanner = class {
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
- function isDir(p) {
311
- try {
312
- return statSync(p).isDirectory();
313
- } catch {
314
- return false;
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
- function isFile(p) {
318
- try {
319
- return statSync(p).isFile();
320
- } catch {
321
- return false;
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
- function sortedDir(p) {
325
- try {
326
- return readdirSync(p).sort();
327
- } catch {
328
- return [];
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
- function readFileSafe(p) {
332
- try {
333
- return readFileSync(p, "utf-8");
334
- } catch {
335
- return "";
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 formatTerminal(result, verbose) {
347
- const lines = [];
348
- const title = "Claude Code 技能掃描報告";
349
- const boxW = 58;
350
- lines.push(pc.cyan("╔" + "═".repeat(boxW) + "╗"));
351
- lines.push(pc.cyan("║") + pc.bold(pc.white(" " + title.padEnd(boxW - 2))) + pc.cyan("║"));
352
- lines.push(pc.cyan("╚" + "═".repeat(boxW) + "╝"));
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
- const result = new Scanner(projectDir).scan(levelFilter);
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.2",
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",