@ikyyofc/gemini-cli 3.0.2 → 3.0.3

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/index.js +79 -80
  2. package/package.json +1 -1
  3. package/src/skills.js +127 -156
package/index.js CHANGED
@@ -25,8 +25,9 @@ import {
25
25
  import { setupGlobalProxy, proxyStatus, setProxyEnabled } from "./src/utils/proxy.js";
26
26
  import { Spinner } from "./src/utils/spinner.js";
27
27
  import {
28
- listInstalledSkills, installSkill, removeSkill,
29
- searchSkills, browseSkills, ensureSkillsDirs, loadSkills,
28
+ listInstalledSkills, installSkill, removeSkillNpx,
29
+ findSkills, listNpxSkills, updateSkill, initSkill,
30
+ ensureSkillsDirs, loadSkills,
30
31
  } from "./src/skills.js";
31
32
 
32
33
  // ─────────────────────────────────────────────────────────────────
@@ -240,131 +241,129 @@ async function handleCommand(input) {
240
241
  const sub = tokens[1]?.toLowerCase();
241
242
  const rest = tokens.slice(2).join(" ").trim();
242
243
 
243
- // /skill list
244
- if (!sub || sub === "list") {
245
- const list = listInstalledSkills();
246
- if (!list.length) {
247
- printInfo("no skills installed · use /skill add <id> to install");
248
- printInfo("browse skills at https://skills.sh");
244
+ // ── /skill list ──────────────────────────────────────
245
+ if (!sub || sub === "list" || sub === "ls") {
246
+ // Quick disk scan (instant, no npx)
247
+ const disk = listInstalledSkills();
248
+ if (!disk.length) {
249
+ printInfo("no skills installed");
250
+ printInfo("browse: https://skills.sh · install: /skill add <owner/repo>");
249
251
  break;
250
252
  }
251
253
  console.log("");
252
- list.forEach(s => {
254
+ disk.forEach(s => {
253
255
  const scope = s.global ? chalk.dim("global") : chalk.hex("#4EC9B0")("project");
254
256
  console.log(
255
- " " + chalk.hex("#FFD080").bold(s.slug.padEnd(28)) +
256
- chalk.dim(s.source.padEnd(30)) + scope
257
+ " " + chalk.hex("#FFD080").bold(s.name.padEnd(30)) +
258
+ chalk.dim(s.slug.padEnd(26)) + scope
257
259
  );
258
260
  });
259
- console.log(chalk.dim(`\n ${list.length} skill(s) loaded into agent context\n`));
261
+ console.log(chalk.dim(`\n ${disk.length} skill(s) active in agent context\n`));
260
262
  break;
261
263
  }
262
264
 
263
- // /skill add <id> [--global]
265
+ // ── /skill add <source> [--global] [--skill <name>] ──
264
266
  if (sub === "add" || sub === "install") {
265
- const isGlobal = rest.includes("--global");
266
- const id = rest.replace("--global", "").trim();
267
- if (!id) { printError("usage: /skill add <owner/repo/slug> [--global]"); break; }
267
+ const isGlobal = rest.includes("--global") || rest.includes("-g");
268
+ const skillMatch = rest.match(/--skill\s+"?([^"]+)"?/);
269
+ const skillName = skillMatch?.[1] ?? null;
270
+ const all = rest.includes("--all");
271
+ const source = rest
272
+ .replace(/--global|-g|--skill\s+"?[^"]*"?|--all/g, "")
273
+ .trim();
274
+
275
+ if (!source) { printError("usage: /skill add <owner/repo> [--global] [--skill <name>] [--all]"); break; }
276
+
268
277
  const sp = new Spinner();
269
- sp.start(`installing ${id}…`, "#FFD080");
278
+ sp.start(`npx skills add ${source}…`, "#FFD080");
270
279
  try {
271
- const result = await installSkill(id, isGlobal ? "global" : "project");
272
- sp.succeed(`installed: ${result.name} → ${result.dir}`);
273
- printInfo("skill will be active on next message");
280
+ const { output } = await installSkill(source, { global: isGlobal, skill: skillName, all });
281
+ sp.stop();
282
+ // Print npx output
283
+ if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
284
+ printSuccess("skill installed — active on next message");
274
285
  } catch (e) {
275
- sp.fail(e.message);
276
- printError(e.message);
286
+ sp.fail(e.message.split("\n")[0]);
287
+ printError(e.message.split("\n")[0]);
277
288
  }
278
289
  break;
279
290
  }
280
291
 
281
- // /skill remove <slug>
292
+ // ── /skill remove <slug> ──────────────────────────────
282
293
  if (sub === "remove" || sub === "rm") {
283
- const slug = rest;
284
- if (!slug) { printError("usage: /skill remove <slug>"); break; }
294
+ if (!rest) { printError("usage: /skill remove <slug>"); break; }
295
+ const sp = new Spinner();
296
+ sp.start(`removing ${rest}…`, "#FF9060");
285
297
  try {
286
- const removed = removeSkill(slug);
287
- removed.forEach(r => printSuccess(`removed: ${r}`));
288
- } catch (e) { printError(e.message); }
298
+ const { output } = await removeSkillNpx(rest);
299
+ sp.stop();
300
+ if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
301
+ printSuccess(`removed: ${rest}`);
302
+ } catch (e) {
303
+ sp.fail(e.message.split("\n")[0]);
304
+ printError(e.message.split("\n")[0]);
305
+ }
289
306
  break;
290
307
  }
291
308
 
292
- // /skill search <query>
293
- if (sub === "search") {
294
- if (!rest) { printError("usage: /skill search <query>"); break; }
309
+ // ── /skill find / search <query> ─────────────────────
310
+ if (sub === "find" || sub === "search") {
311
+ if (!rest) { printError("usage: /skill find <query>"); break; }
295
312
  const sp = new Spinner();
296
313
  sp.start(`searching "${rest}"…`, "#4A9EFF");
297
314
  try {
298
- const results = await searchSkills(rest, 8);
315
+ const output = await findSkills(rest);
299
316
  sp.stop();
300
- if (!results.length) { printInfo("no results found"); break; }
301
- console.log("");
302
- results.forEach(r => {
303
- console.log(
304
- " " + chalk.hex("#4A9EFF").bold(r.id.padEnd(48)) +
305
- chalk.dim(String(r.installs).padStart(8) + " installs")
306
- );
307
- if (r.name !== r.slug)
308
- console.log(" " + chalk.dim(" ".repeat(2) + r.name));
309
- });
310
- console.log(chalk.dim("\n install: /skill add <id>\n"));
317
+ if (output) {
318
+ console.log("");
319
+ output.split("\n").forEach(l => console.log(" " + l));
320
+ console.log("");
321
+ } else {
322
+ printInfo("no results try a different query");
323
+ }
311
324
  } catch (e) {
312
- sp.fail(e.message);
313
- printError(e.message);
325
+ sp.fail(e.message.split("\n")[0]);
326
+ printError(e.message.split("\n")[0]);
314
327
  }
315
328
  break;
316
329
  }
317
330
 
318
- // /skill browse [trending|hot]
319
- if (sub === "browse") {
320
- const view = rest || "all-time";
321
- const sp = new Spinner();
322
- sp.start(`fetching ${view}…`, "#4A9EFF");
331
+ // ── /skill update [slug] ──────────────────────────────
332
+ if (sub === "update") {
333
+ const sp = new Spinner();
334
+ sp.start(rest ? `updating ${rest}…` : "updating all skills…", "#4A9EFF");
323
335
  try {
324
- const results = await browseSkills(view, 15);
336
+ const { output } = await updateSkill(rest);
325
337
  sp.stop();
326
- console.log("");
327
- results.forEach((r, i) => {
328
- console.log(
329
- " " + chalk.dim(String(i+1).padStart(2) + ".") + " " +
330
- chalk.hex("#4A9EFF").bold(r.id.padEnd(46)) +
331
- chalk.dim(String(r.installs).padStart(8) + " installs")
332
- );
333
- });
334
- console.log(chalk.dim("\n /skill browse trending · /skill browse hot\n"));
338
+ if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
339
+ printSuccess("updated");
335
340
  } catch (e) {
336
- sp.fail(e.message);
337
- printError(e.message);
341
+ sp.fail(e.message.split("\n")[0]);
342
+ printError(e.message.split("\n")[0]);
338
343
  }
339
344
  break;
340
345
  }
341
346
 
342
- // /skill info <id>
343
- if (sub === "info") {
344
- if (!rest) { printError("usage: /skill info <id>"); break; }
347
+ // ── /skill init [name] ────────────────────────────────
348
+ if (sub === "init") {
345
349
  const sp = new Spinner();
346
- sp.start(`fetching ${rest}…`, "#4A9EFF");
350
+ sp.start("creating SKILL.md template…", "#4A9EFF");
347
351
  try {
348
- const { default: ax } = await import("axios");
349
- const { data } = await ax.get(`https://skills.sh/api/v1/skills/${rest}`);
352
+ const { output } = await initSkill(rest);
350
353
  sp.stop();
351
- console.log("");
352
- printInfo(`id ${data.id}`);
353
- printInfo(`source ${data.source}`);
354
- printInfo(`installs ${data.installs}`);
355
- printInfo(`files ${data.files?.length ?? 0}`);
356
- if (data.files?.length) {
357
- data.files.forEach(f => console.log(chalk.dim(" " + f.path)));
358
- }
359
- console.log(chalk.dim(`\n /skill add ${data.id}\n`));
354
+ if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
355
+ printSuccess("SKILL.md created");
360
356
  } catch (e) {
361
- sp.fail(e.message);
362
- printError(e.message);
357
+ sp.fail(e.message.split("\n")[0]);
358
+ printError(e.message.split("\n")[0]);
363
359
  }
364
360
  break;
365
361
  }
366
362
 
367
- printInfo("usage: /skill [list | add | remove | search | browse | info]");
363
+ printInfo("usage: /skill [list | add | remove | find | update | init]");
364
+ printInfo(" /skill add vercel-labs/agent-skills");
365
+ printInfo(" /skill add anthropics/skills --skill frontend-design");
366
+ printInfo(" /skill add owner/repo --global");
368
367
  break;
369
368
  }
370
369
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikyyofc/gemini-cli",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
4
4
  "description": "AI Agent CLI — native function calling · GEMINI.md context · extensions",
5
5
  "type": "module",
6
6
  "bin": { "gemini": "./index.js" },
package/src/skills.js CHANGED
@@ -1,218 +1,189 @@
1
- // src/skills.js — Agent Skills Manager
2
- // Integrates with skills.sh registry
3
- // Skills are stored in .agents/ (project) or ~/.gemini/agents/ (global)
1
+ // src/skills.js — Skills manager via npx skills CLI
2
+ // All install/search/list/remove operations delegate to "npx skills"
3
+ // which is the official skills.sh CLI (github.com/vercel-labs/skills)
4
+ //
5
+ // Skills directories (Gemini agent convention from skills.sh):
6
+ // Project: ./.gemini/skills/ (committed with project)
7
+ // Global: ~/.gemini/skills/ (across all projects)
8
+ // Custom: ./.agents/ (manual / local skills)
4
9
  import fs from "fs";
5
10
  import path from "path";
6
11
  import os from "os";
7
- import axios from "axios";
12
+ import { promisify } from "util";
13
+ import { exec } from "child_process";
8
14
 
9
- const API = "https://skills.sh/api/v1";
10
- const GLOBAL_AGENTS_DIR = path.join(os.homedir(), ".gemini", "agents");
15
+ const execAsync = promisify(exec);
11
16
 
12
- // ─────────────────────────────────────────────────────────────────
13
- // Resolve skills directories
14
- // Priority: .agents/ (project) ~/.gemini/agents/ (global)
15
- // ─────────────────────────────────────────────────────────────────
16
- export function getSkillsDirs() {
17
- const dirs = [];
18
- const local = path.join(process.cwd(), ".agents");
19
- if (fs.existsSync(local)) dirs.push(local);
20
- if (fs.existsSync(GLOBAL_AGENTS_DIR)) dirs.push(GLOBAL_AGENTS_DIR);
21
- return dirs;
22
- }
17
+ // Directories where npx skills installs for Gemini agent
18
+ const GLOBAL_SKILLS_DIR = path.join(os.homedir(), ".gemini", "skills");
19
+ const PROJECT_SKILLS_DIR = () => path.join(process.cwd(), ".gemini", "skills");
20
+ const CUSTOM_SKILLS_DIR = () => path.join(process.cwd(), ".agents");
23
21
 
24
22
  export function ensureSkillsDirs() {
25
- fs.mkdirSync(GLOBAL_AGENTS_DIR, { recursive: true });
23
+ fs.mkdirSync(GLOBAL_SKILLS_DIR, { recursive: true });
26
24
  }
27
25
 
28
26
  // ─────────────────────────────────────────────────────────────────
29
- // Load all installed skills returns array of { name, path, content }
30
- // ─────────────────────────────────────────────────────────────────
31
- export function loadSkills() {
32
- const skills = [];
33
- const seen = new Set();
34
-
35
- for (const dir of getSkillsDirs()) {
36
- if (!fs.existsSync(dir)) continue;
37
-
38
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
39
- if (!entry.isDirectory()) continue;
40
- const skillDir = path.join(dir, entry.name);
41
- const skillMd = path.join(skillDir, "SKILL.md");
42
- const metaFile = path.join(skillDir, ".meta.json");
43
-
44
- if (!fs.existsSync(skillMd)) continue;
45
- if (seen.has(entry.name)) continue; // project-level takes priority
46
- seen.add(entry.name);
47
-
48
- let meta = {};
49
- try { meta = JSON.parse(fs.readFileSync(metaFile, "utf8")); } catch {}
50
-
51
- const content = fs.readFileSync(skillMd, "utf8");
52
-
53
- // Also read any supporting files (examples, etc.)
54
- const extras = fs.readdirSync(skillDir)
55
- .filter(f => f !== "SKILL.md" && f !== ".meta.json" && f.endsWith(".md"))
56
- .map(f => fs.readFileSync(path.join(skillDir, f), "utf8"))
57
- .join("\n\n");
58
-
59
- skills.push({
60
- name: meta.name ?? entry.name,
61
- slug: meta.slug ?? entry.name,
62
- source: meta.source ?? "local",
63
- path: skillDir,
64
- content: content + (extras ? "\n\n" + extras : ""),
65
- global: dir === GLOBAL_AGENTS_DIR,
66
- });
67
- }
68
- }
69
-
70
- return skills;
27
+ // Run npx skills with timeout
28
+ // ─────────────────────────────────────────────────────────────────
29
+ async function npxSkills(args, cwd = process.cwd(), timeout = 120_000) {
30
+ const cmd = `npx --yes skills ${args}`;
31
+ const { stdout, stderr } = await execAsync(cmd, {
32
+ cwd,
33
+ timeout,
34
+ env: {
35
+ ...process.env,
36
+ // Disable telemetry analytics
37
+ DISABLE_TELEMETRY: "1",
38
+ // Force non-interactive / no color for parsing
39
+ NO_COLOR: "1",
40
+ CI: "1",
41
+ },
42
+ });
43
+ return { stdout: stdout.trim(), stderr: stderr.trim() };
71
44
  }
72
45
 
73
46
  // ─────────────────────────────────────────────────────────────────
74
- // Build skills section for system prompt
47
+ // Install — npx skills add <source> -a gemini -y [--global]
75
48
  // ─────────────────────────────────────────────────────────────────
76
- export function buildSkillsPrompt(skills) {
77
- if (!skills.length) return null;
49
+ export async function installSkill(source, opts = {}) {
50
+ const {
51
+ global = false,
52
+ skill = null, // specific skill name (--skill flag)
53
+ all = false, // install all skills from repo
54
+ } = opts;
78
55
 
79
- const sections = skills.map(s =>
80
- `### Skill: ${s.name}\n${s.content.trim()}`
81
- ).join("\n\n---\n\n");
56
+ let args = `add ${source} -a gemini -y --copy`;
57
+ if (global) args += " -g";
58
+ if (skill) args += ` --skill "${skill}"`;
59
+ if (all) args += " --skill '*'";
82
60
 
83
- return `## INSTALLED SKILLS\n\nThe following skills provide additional knowledge and capabilities. Apply them when relevant:\n\n${sections}`;
61
+ const { stdout, stderr } = await npxSkills(args);
62
+ return { output: stdout || stderr };
84
63
  }
85
64
 
86
65
  // ─────────────────────────────────────────────────────────────────
87
- // API helpers
66
+ // List installed — npx skills list
88
67
  // ─────────────────────────────────────────────────────────────────
89
- async function apiFetch(path, params = {}) {
90
- const url = `${API}${path}`;
91
- const res = await axios.get(url, {
92
- params,
93
- headers: { "User-Agent": "gemini-cli/2.0" },
94
- timeout: 15000,
95
- });
96
- return res.data;
68
+ export async function listNpxSkills(global = false) {
69
+ const args = global ? "list -g" : "list";
70
+ const { stdout } = await npxSkills(args);
71
+ return stdout;
97
72
  }
98
73
 
99
74
  // ─────────────────────────────────────────────────────────────────
100
- // Search skills
75
+ // Search — npx skills find <query>
101
76
  // ─────────────────────────────────────────────────────────────────
102
- export async function searchSkills(query, limit = 10) {
103
- const data = await apiFetch("/skills/search", { q: query, limit });
104
- return data.data ?? [];
77
+ export async function findSkills(query) {
78
+ const { stdout } = await npxSkills(`find ${JSON.stringify(query)}`);
79
+ return stdout;
105
80
  }
106
81
 
107
- export async function browseSkills(view = "all-time", perPage = 20) {
108
- const data = await apiFetch("/skills", { view, per_page: perPage });
109
- return data.data ?? [];
82
+ // ─────────────────────────────────────────────────────────────────
83
+ // Remove npx skills remove <slug>
84
+ // ─────────────────────────────────────────────────────────────────
85
+ export async function removeSkillNpx(slug) {
86
+ const { stdout, stderr } = await npxSkills(`remove ${slug} -y`);
87
+ return { output: stdout || stderr };
110
88
  }
111
89
 
112
90
  // ─────────────────────────────────────────────────────────────────
113
- // Fetch skill files from API
114
- // id format: "owner/repo/slug" e.g. "vercel-labs/agent-skills/next-js-development"
91
+ // Update npx skills update [slug]
115
92
  // ─────────────────────────────────────────────────────────────────
116
- export async function fetchSkill(id) {
117
- const data = await apiFetch(`/skills/${id}`);
118
- return data; // { id, slug, name?, files: [{ path, contents }] }
93
+ export async function updateSkill(slug = "") {
94
+ const args = slug ? `update ${slug} -y` : "update -y";
95
+ const { stdout, stderr } = await npxSkills(args);
96
+ return { output: stdout || stderr };
119
97
  }
120
98
 
121
99
  // ─────────────────────────────────────────────────────────────────
122
- // Install a skill
123
- // source: "owner/repo/slug" or just "owner/repo" (installs all)
124
- // scope: "project" | "global"
125
- // ─────────────────────────────────────────────────────────────────
126
- export async function installSkill(id, scope = "project") {
127
- const skill = await fetchSkill(id);
128
-
129
- if (!skill.files?.length) {
130
- throw new Error(`Skill "${id}" has no files.`);
131
- }
132
-
133
- const targetDir = scope === "global"
134
- ? GLOBAL_AGENTS_DIR
135
- : path.join(process.cwd(), ".agents");
136
-
137
- const slug = skill.slug ?? id.split("/").pop();
138
- const skillDir = path.join(targetDir, slug);
139
- fs.mkdirSync(skillDir, { recursive: true });
140
-
141
- // Write all skill files
142
- for (const file of skill.files) {
143
- const filePath = path.join(skillDir, file.path);
144
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
145
- fs.writeFileSync(filePath, file.contents ?? "", "utf8");
146
- }
147
-
148
- // Write metadata
149
- fs.writeFileSync(path.join(skillDir, ".meta.json"), JSON.stringify({
150
- id: skill.id,
151
- name: skill.name ?? slug,
152
- slug,
153
- source: skill.source,
154
- installs: skill.installs,
155
- installedAt: new Date().toISOString(),
156
- scope,
157
- }, null, 2));
158
-
159
- return { slug, name: skill.name ?? slug, dir: skillDir };
100
+ // Init — npx skills init [name] (creates a SKILL.md template)
101
+ // ─────────────────────────────────────────────────────────────────
102
+ export async function initSkill(name = "") {
103
+ const args = name ? `init "${name}"` : "init";
104
+ const { stdout, stderr } = await npxSkills(args);
105
+ return { output: stdout || stderr };
160
106
  }
161
107
 
162
108
  // ─────────────────────────────────────────────────────────────────
163
- // Remove a skill
109
+ // Load skills for agent context injection
110
+ // Scans all skill directories and reads SKILL.md files
164
111
  // ─────────────────────────────────────────────────────────────────
165
- export function removeSkill(slug, scope = "both") {
166
- const removed = [];
112
+ export function loadSkills() {
113
+ const skills = [];
114
+ const seen = new Set();
167
115
 
168
- const dirs = scope === "global"
169
- ? [GLOBAL_AGENTS_DIR]
170
- : scope === "project"
171
- ? [path.join(process.cwd(), ".agents")]
172
- : [path.join(process.cwd(), ".agents"), GLOBAL_AGENTS_DIR];
116
+ const scanDir = (dir, scope) => {
117
+ if (!fs.existsSync(dir)) return;
118
+
119
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
120
+ for (const entry of entries) {
121
+ if (!entry.isDirectory()) continue;
173
122
 
174
- for (const dir of dirs) {
175
- const skillDir = path.join(dir, slug);
176
- if (fs.existsSync(skillDir)) {
177
- fs.rmSync(skillDir, { recursive: true, force: true });
178
- removed.push(skillDir);
123
+ const skillDir = path.join(dir, entry.name);
124
+ const skillMd = path.join(skillDir, "SKILL.md");
125
+ if (!fs.existsSync(skillMd)) continue;
126
+
127
+ const key = entry.name;
128
+ if (seen.has(key)) continue; // project takes priority over global
129
+ seen.add(key);
130
+
131
+ const content = fs.readFileSync(skillMd, "utf8");
132
+
133
+ // Parse optional frontmatter name from SKILL.md
134
+ const nameMatch = content.match(/^#\s+(.+)$/m);
135
+ const name = nameMatch?.[1]?.trim() ?? entry.name;
136
+
137
+ skills.push({ name, slug: entry.name, content, scope, path: skillDir });
179
138
  }
180
- }
139
+ };
181
140
 
182
- if (!removed.length) throw new Error(`Skill "${slug}" not found.`);
183
- return removed;
141
+ // Priority: project (.gemini/skills/) > custom (.agents/) > global (~/.gemini/skills/)
142
+ scanDir(PROJECT_SKILLS_DIR(), "project");
143
+ scanDir(CUSTOM_SKILLS_DIR(), "custom");
144
+ scanDir(GLOBAL_SKILLS_DIR, "global");
145
+
146
+ return skills;
184
147
  }
185
148
 
186
149
  // ─────────────────────────────────────────────────────────────────
187
- // List installed skills with status
150
+ // Build skills section injected into agent system prompt
151
+ // ─────────────────────────────────────────────────────────────────
152
+ export function buildSkillsPrompt(skills) {
153
+ if (!skills.length) return null;
154
+
155
+ const sections = skills.map(s =>
156
+ `### Skill: ${s.name}\n${s.content.trim()}`
157
+ ).join("\n\n---\n\n");
158
+
159
+ return `## INSTALLED SKILLS\n\nApply the following skills when relevant to the current task:\n\n${sections}`;
160
+ }
161
+
162
+ // ─────────────────────────────────────────────────────────────────
163
+ // Quick disk check — list installed skills without running npx
188
164
  // ─────────────────────────────────────────────────────────────────
189
165
  export function listInstalledSkills() {
190
166
  const result = [];
191
167
 
192
- const checkDir = (dir, isGlobal) => {
168
+ const scanDir = (dir, isGlobal) => {
193
169
  if (!fs.existsSync(dir)) return;
194
170
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
195
171
  if (!entry.isDirectory()) continue;
196
172
  const skillDir = path.join(dir, entry.name);
197
- const metaFile = path.join(skillDir, ".meta.json");
198
- const hasSkillMd = fs.existsSync(path.join(skillDir, "SKILL.md"));
199
- if (!hasSkillMd) continue;
200
-
201
- let meta = {};
202
- try { meta = JSON.parse(fs.readFileSync(metaFile, "utf8")); } catch {}
203
-
173
+ if (!fs.existsSync(path.join(skillDir, "SKILL.md"))) continue;
174
+ const content = fs.readFileSync(path.join(skillDir, "SKILL.md"), "utf8");
175
+ const nameMatch = content.match(/^#\s+(.+)$/m);
204
176
  result.push({
205
177
  slug: entry.name,
206
- name: meta.name ?? entry.name,
207
- source: meta.source ?? "local",
178
+ name: nameMatch?.[1]?.trim() ?? entry.name,
208
179
  global: isGlobal,
209
180
  dir: skillDir,
210
- installedAt: meta.installedAt ?? null,
211
181
  });
212
182
  }
213
183
  };
214
184
 
215
- checkDir(path.join(process.cwd(), ".agents"), false);
216
- checkDir(GLOBAL_AGENTS_DIR, true);
185
+ scanDir(PROJECT_SKILLS_DIR(), false);
186
+ scanDir(CUSTOM_SKILLS_DIR(), false);
187
+ scanDir(GLOBAL_SKILLS_DIR, true);
217
188
  return result;
218
189
  }