@ikyyofc/gemini-cli 3.0.2 → 3.0.4

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 +82 -80
  2. package/package.json +1 -1
  3. package/src/skills.js +136 -150
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,132 @@ 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
+ if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
283
+ printSuccess("skill installed — active on next message");
274
284
  } catch (e) {
275
- sp.fail(e.message);
276
- printError(e.message);
285
+ sp.stop();
286
+ // Print full npx output so user sees what actually went wrong
287
+ e.message.split("\n").filter(Boolean).forEach(l =>
288
+ process.stdout.write(chalk.hex("#7A7A9A")(" " + l + "\n"))
289
+ );
290
+ printError("install failed");
277
291
  }
278
292
  break;
279
293
  }
280
294
 
281
- // /skill remove <slug>
295
+ // ── /skill remove <slug> ──────────────────────────────
282
296
  if (sub === "remove" || sub === "rm") {
283
- const slug = rest;
284
- if (!slug) { printError("usage: /skill remove <slug>"); break; }
297
+ if (!rest) { printError("usage: /skill remove <slug>"); break; }
298
+ const sp = new Spinner();
299
+ sp.start(`removing ${rest}…`, "#FF9060");
285
300
  try {
286
- const removed = removeSkill(slug);
287
- removed.forEach(r => printSuccess(`removed: ${r}`));
288
- } catch (e) { printError(e.message); }
301
+ const { output } = await removeSkillNpx(rest);
302
+ sp.stop();
303
+ if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
304
+ printSuccess(`removed: ${rest}`);
305
+ } catch (e) {
306
+ sp.fail(e.message.split("\n")[0]);
307
+ printError(e.message.split("\n")[0]);
308
+ }
289
309
  break;
290
310
  }
291
311
 
292
- // /skill search <query>
293
- if (sub === "search") {
294
- if (!rest) { printError("usage: /skill search <query>"); break; }
312
+ // ── /skill find / search <query> ─────────────────────
313
+ if (sub === "find" || sub === "search") {
314
+ if (!rest) { printError("usage: /skill find <query>"); break; }
295
315
  const sp = new Spinner();
296
316
  sp.start(`searching "${rest}"…`, "#4A9EFF");
297
317
  try {
298
- const results = await searchSkills(rest, 8);
318
+ const output = await findSkills(rest);
299
319
  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"));
320
+ if (output) {
321
+ console.log("");
322
+ output.split("\n").forEach(l => console.log(" " + l));
323
+ console.log("");
324
+ } else {
325
+ printInfo("no results try a different query");
326
+ }
311
327
  } catch (e) {
312
- sp.fail(e.message);
313
- printError(e.message);
328
+ sp.fail(e.message.split("\n")[0]);
329
+ printError(e.message.split("\n")[0]);
314
330
  }
315
331
  break;
316
332
  }
317
333
 
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");
334
+ // ── /skill update [slug] ──────────────────────────────
335
+ if (sub === "update") {
336
+ const sp = new Spinner();
337
+ sp.start(rest ? `updating ${rest}…` : "updating all skills…", "#4A9EFF");
323
338
  try {
324
- const results = await browseSkills(view, 15);
339
+ const { output } = await updateSkill(rest);
325
340
  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"));
341
+ if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
342
+ printSuccess("updated");
335
343
  } catch (e) {
336
- sp.fail(e.message);
337
- printError(e.message);
344
+ sp.fail(e.message.split("\n")[0]);
345
+ printError(e.message.split("\n")[0]);
338
346
  }
339
347
  break;
340
348
  }
341
349
 
342
- // /skill info <id>
343
- if (sub === "info") {
344
- if (!rest) { printError("usage: /skill info <id>"); break; }
350
+ // ── /skill init [name] ────────────────────────────────
351
+ if (sub === "init") {
345
352
  const sp = new Spinner();
346
- sp.start(`fetching ${rest}…`, "#4A9EFF");
353
+ sp.start("creating SKILL.md template…", "#4A9EFF");
347
354
  try {
348
- const { default: ax } = await import("axios");
349
- const { data } = await ax.get(`https://skills.sh/api/v1/skills/${rest}`);
355
+ const { output } = await initSkill(rest);
350
356
  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`));
357
+ if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
358
+ printSuccess("SKILL.md created");
360
359
  } catch (e) {
361
- sp.fail(e.message);
362
- printError(e.message);
360
+ sp.fail(e.message.split("\n")[0]);
361
+ printError(e.message.split("\n")[0]);
363
362
  }
364
363
  break;
365
364
  }
366
365
 
367
- printInfo("usage: /skill [list | add | remove | search | browse | info]");
366
+ printInfo("usage: /skill [list | add | remove | find | update | init]");
367
+ printInfo(" /skill add vercel-labs/agent-skills");
368
+ printInfo(" /skill add anthropics/skills --skill frontend-design");
369
+ printInfo(" /skill add owner/repo --global");
368
370
  break;
369
371
  }
370
372
 
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.4",
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,204 @@
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
- }
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
+ try {
32
+ const { stdout, stderr } = await execAsync(cmd, {
33
+ cwd,
34
+ timeout,
35
+ env: { ...process.env, NO_COLOR: "1", CI: "1" },
36
+ });
37
+ return { stdout: stdout.trim(), stderr: stderr.trim() };
38
+ } catch (err) {
39
+ // Expose actual npx output in the thrown error message
40
+ const detail = [err.stdout?.trim(), err.stderr?.trim()]
41
+ .filter(Boolean).join("\n") || err.message;
42
+ throw new Error(detail);
68
43
  }
44
+ }
69
45
 
70
- return skills;
46
+ // ─────────────────────────────────────────────────────────────────
47
+ // Parse source: handles "owner/repo@skill-name" shorthand
48
+ // e.g. "anthropics/skills@frontend-design"
49
+ // → source="anthropics/skills", skill="frontend-design"
50
+ // ─────────────────────────────────────────────────────────────────
51
+ function parseSource(raw) {
52
+ const atIdx = raw.indexOf("@");
53
+ if (atIdx > 0 && !raw.startsWith("http") && !raw.startsWith("git@")) {
54
+ return {
55
+ source: raw.slice(0, atIdx),
56
+ skill: raw.slice(atIdx + 1),
57
+ };
58
+ }
59
+ return { source: raw, skill: null };
71
60
  }
72
61
 
73
62
  // ─────────────────────────────────────────────────────────────────
74
- // Build skills section for system prompt
63
+ // Install — npx skills add <source> [-y] [--global] [--skill name]
75
64
  // ─────────────────────────────────────────────────────────────────
76
- export function buildSkillsPrompt(skills) {
77
- if (!skills.length) return null;
65
+ export async function installSkill(rawSource, opts = {}) {
66
+ const { global = false, skill: explicitSkill = null, all = false } = opts;
67
+ const { source, skill: parsedSkill } = parseSource(rawSource);
68
+ const skillName = explicitSkill ?? parsedSkill;
78
69
 
79
- const sections = skills.map(s =>
80
- `### Skill: ${s.name}\n${s.content.trim()}`
81
- ).join("\n\n---\n\n");
70
+ // Build args — let npx skills auto-detect agent (more reliable than -a gemini)
71
+ const parts = ["add", source, "-y"];
72
+ if (global) parts.push("-g");
73
+ if (skillName) parts.push(`--skill "${skillName}"`);
74
+ if (all) parts.push("--skill '*'");
75
+
76
+ const { stdout, stderr } = await npxSkills(parts.join(" "));
77
+ return { output: stdout || stderr };
78
+ }
82
79
 
83
- return `## INSTALLED SKILLS\n\nThe following skills provide additional knowledge and capabilities. Apply them when relevant:\n\n${sections}`;
80
+ // ─────────────────────────────────────────────────────────────────
81
+ // List installed — npx skills list
82
+ // ─────────────────────────────────────────────────────────────────
83
+ export async function listNpxSkills(global = false) {
84
+ const args = global ? "list -g" : "list";
85
+ const { stdout } = await npxSkills(args);
86
+ return stdout;
84
87
  }
85
88
 
86
89
  // ─────────────────────────────────────────────────────────────────
87
- // API helpers
90
+ // Search — npx skills find <query>
88
91
  // ─────────────────────────────────────────────────────────────────
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;
92
+ export async function findSkills(query) {
93
+ const { stdout } = await npxSkills(`find ${JSON.stringify(query)}`);
94
+ return stdout;
97
95
  }
98
96
 
99
97
  // ─────────────────────────────────────────────────────────────────
100
- // Search skills
98
+ // Remove — npx skills remove <slug>
101
99
  // ─────────────────────────────────────────────────────────────────
102
- export async function searchSkills(query, limit = 10) {
103
- const data = await apiFetch("/skills/search", { q: query, limit });
104
- return data.data ?? [];
100
+ export async function removeSkillNpx(slug) {
101
+ const { stdout, stderr } = await npxSkills(`remove ${slug} -y`);
102
+ return { output: stdout || stderr };
105
103
  }
106
104
 
107
- export async function browseSkills(view = "all-time", perPage = 20) {
108
- const data = await apiFetch("/skills", { view, per_page: perPage });
109
- return data.data ?? [];
105
+ // ─────────────────────────────────────────────────────────────────
106
+ // Update npx skills update [slug]
107
+ // ─────────────────────────────────────────────────────────────────
108
+ export async function updateSkill(slug = "") {
109
+ const args = slug ? `update ${slug} -y` : "update -y";
110
+ const { stdout, stderr } = await npxSkills(args);
111
+ return { output: stdout || stderr };
110
112
  }
111
113
 
112
114
  // ─────────────────────────────────────────────────────────────────
113
- // Fetch skill files from API
114
- // id format: "owner/repo/slug" e.g. "vercel-labs/agent-skills/next-js-development"
115
+ // Init npx skills init [name] (creates a SKILL.md template)
115
116
  // ─────────────────────────────────────────────────────────────────
116
- export async function fetchSkill(id) {
117
- const data = await apiFetch(`/skills/${id}`);
118
- return data; // { id, slug, name?, files: [{ path, contents }] }
117
+ export async function initSkill(name = "") {
118
+ const args = name ? `init "${name}"` : "init";
119
+ const { stdout, stderr } = await npxSkills(args);
120
+ return { output: stdout || stderr };
119
121
  }
120
122
 
121
123
  // ─────────────────────────────────────────────────────────────────
122
- // Install a skill
123
- // source: "owner/repo/slug" or just "owner/repo" (installs all)
124
- // scope: "project" | "global"
124
+ // Load skills for agent context injection
125
+ // Scans all skill directories and reads SKILL.md files
125
126
  // ─────────────────────────────────────────────────────────────────
126
- export async function installSkill(id, scope = "project") {
127
- const skill = await fetchSkill(id);
127
+ export function loadSkills() {
128
+ const skills = [];
129
+ const seen = new Set();
128
130
 
129
- if (!skill.files?.length) {
130
- throw new Error(`Skill "${id}" has no files.`);
131
- }
131
+ const scanDir = (dir, scope) => {
132
+ if (!fs.existsSync(dir)) return;
133
+
134
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
135
+ for (const entry of entries) {
136
+ if (!entry.isDirectory()) continue;
137
+
138
+ const skillDir = path.join(dir, entry.name);
139
+ const skillMd = path.join(skillDir, "SKILL.md");
140
+ if (!fs.existsSync(skillMd)) continue;
132
141
 
133
- const targetDir = scope === "global"
134
- ? GLOBAL_AGENTS_DIR
135
- : path.join(process.cwd(), ".agents");
142
+ const key = entry.name;
143
+ if (seen.has(key)) continue; // project takes priority over global
144
+ seen.add(key);
136
145
 
137
- const slug = skill.slug ?? id.split("/").pop();
138
- const skillDir = path.join(targetDir, slug);
139
- fs.mkdirSync(skillDir, { recursive: true });
146
+ const content = fs.readFileSync(skillMd, "utf8");
140
147
 
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
- }
148
+ // Parse optional frontmatter name from SKILL.md
149
+ const nameMatch = content.match(/^#\s+(.+)$/m);
150
+ const name = nameMatch?.[1]?.trim() ?? entry.name;
147
151
 
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 };
152
+ skills.push({ name, slug: entry.name, content, scope, path: skillDir });
153
+ }
154
+ };
155
+
156
+ // Priority: project (.gemini/skills/) > custom (.agents/) > global (~/.gemini/skills/)
157
+ scanDir(PROJECT_SKILLS_DIR(), "project");
158
+ scanDir(CUSTOM_SKILLS_DIR(), "custom");
159
+ scanDir(GLOBAL_SKILLS_DIR, "global");
160
+
161
+ return skills;
160
162
  }
161
163
 
162
164
  // ─────────────────────────────────────────────────────────────────
163
- // Remove a skill
165
+ // Build skills section injected into agent system prompt
164
166
  // ─────────────────────────────────────────────────────────────────
165
- export function removeSkill(slug, scope = "both") {
166
- const removed = [];
167
-
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];
167
+ export function buildSkillsPrompt(skills) {
168
+ if (!skills.length) return null;
173
169
 
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);
179
- }
180
- }
170
+ const sections = skills.map(s =>
171
+ `### Skill: ${s.name}\n${s.content.trim()}`
172
+ ).join("\n\n---\n\n");
181
173
 
182
- if (!removed.length) throw new Error(`Skill "${slug}" not found.`);
183
- return removed;
174
+ return `## INSTALLED SKILLS\n\nApply the following skills when relevant to the current task:\n\n${sections}`;
184
175
  }
185
176
 
186
177
  // ─────────────────────────────────────────────────────────────────
187
- // List installed skills with status
178
+ // Quick disk check — list installed skills without running npx
188
179
  // ─────────────────────────────────────────────────────────────────
189
180
  export function listInstalledSkills() {
190
181
  const result = [];
191
182
 
192
- const checkDir = (dir, isGlobal) => {
183
+ const scanDir = (dir, isGlobal) => {
193
184
  if (!fs.existsSync(dir)) return;
194
185
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
195
186
  if (!entry.isDirectory()) continue;
196
187
  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
-
188
+ if (!fs.existsSync(path.join(skillDir, "SKILL.md"))) continue;
189
+ const content = fs.readFileSync(path.join(skillDir, "SKILL.md"), "utf8");
190
+ const nameMatch = content.match(/^#\s+(.+)$/m);
204
191
  result.push({
205
192
  slug: entry.name,
206
- name: meta.name ?? entry.name,
207
- source: meta.source ?? "local",
193
+ name: nameMatch?.[1]?.trim() ?? entry.name,
208
194
  global: isGlobal,
209
195
  dir: skillDir,
210
- installedAt: meta.installedAt ?? null,
211
196
  });
212
197
  }
213
198
  };
214
199
 
215
- checkDir(path.join(process.cwd(), ".agents"), false);
216
- checkDir(GLOBAL_AGENTS_DIR, true);
200
+ scanDir(PROJECT_SKILLS_DIR(), false);
201
+ scanDir(CUSTOM_SKILLS_DIR(), false);
202
+ scanDir(GLOBAL_SKILLS_DIR, true);
217
203
  return result;
218
204
  }