@ikyyofc/gemini-cli 3.0.4 → 3.0.6

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/skills.js +128 -112
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikyyofc/gemini-cli",
3
- "version": "3.0.4",
3
+ "version": "3.0.6",
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,11 +1,5 @@
1
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)
2
+ // All skills are consolidated into a single path: ~/.agents/
9
3
  import fs from "fs";
10
4
  import path from "path";
11
5
  import os from "os";
@@ -14,29 +8,54 @@ import { exec } from "child_process";
14
8
 
15
9
  const execAsync = promisify(exec);
16
10
 
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");
11
+ // Single canonical location for ALL skills
12
+ export const AGENTS_DIR = path.join(os.homedir(), ".agents");
13
+
14
+ // Agent dirs that npx skills may create (we scan after install to find new files)
15
+ const KNOWN_AGENT_DIRS = [
16
+ ".gemini", ".gemini/skills",
17
+ ".claude", ".claude/skills",
18
+ ".cursor", ".cursor/rules",
19
+ ".codex", ".opencode",
20
+ ".cline", ".aider",
21
+ ".amp", ".copilot",
22
+ ];
21
23
 
22
24
  export function ensureSkillsDirs() {
23
- fs.mkdirSync(GLOBAL_SKILLS_DIR, { recursive: true });
25
+ fs.mkdirSync(AGENTS_DIR, { recursive: true });
26
+ }
27
+
28
+ // ─────────────────────────────────────────────────────────────────
29
+ // Scan a dir tree for SKILL.md files
30
+ // ─────────────────────────────────────────────────────────────────
31
+ function scanForSkillMds(baseDir, maxDepth = 5) {
32
+ const results = [];
33
+ const SKIP = new Set(["node_modules", ".git", "dist", "build"]);
34
+ const walk = (dir, depth) => {
35
+ if (depth > maxDepth || !fs.existsSync(dir)) return;
36
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
37
+ if (SKIP.has(entry.name)) continue;
38
+ const full = path.join(dir, entry.name);
39
+ if (entry.isDirectory()) walk(full, depth + 1);
40
+ else if (entry.name === "SKILL.md") results.push(full);
41
+ }
42
+ };
43
+ walk(baseDir, 0);
44
+ return results;
24
45
  }
25
46
 
26
47
  // ─────────────────────────────────────────────────────────────────
27
- // Run npx skills with timeout
48
+ // Run npx skills exposes real stderr on failure
28
49
  // ─────────────────────────────────────────────────────────────────
29
50
  async function npxSkills(args, cwd = process.cwd(), timeout = 120_000) {
30
51
  const cmd = `npx --yes skills ${args}`;
31
52
  try {
32
53
  const { stdout, stderr } = await execAsync(cmd, {
33
- cwd,
34
- timeout,
54
+ cwd, timeout,
35
55
  env: { ...process.env, NO_COLOR: "1", CI: "1" },
36
56
  });
37
57
  return { stdout: stdout.trim(), stderr: stderr.trim() };
38
58
  } catch (err) {
39
- // Expose actual npx output in the thrown error message
40
59
  const detail = [err.stdout?.trim(), err.stderr?.trim()]
41
60
  .filter(Boolean).join("\n") || err.message;
42
61
  throw new Error(detail);
@@ -44,161 +63,158 @@ async function npxSkills(args, cwd = process.cwd(), timeout = 120_000) {
44
63
  }
45
64
 
46
65
  // ─────────────────────────────────────────────────────────────────
47
- // Parse source: handles "owner/repo@skill-name" shorthand
48
- // e.g. "anthropics/skills@frontend-design"
49
- // → source="anthropics/skills", skill="frontend-design"
66
+ // Parse "owner/repo@skill-name" shorthand
50
67
  // ─────────────────────────────────────────────────────────────────
51
68
  function parseSource(raw) {
52
69
  const atIdx = raw.indexOf("@");
53
70
  if (atIdx > 0 && !raw.startsWith("http") && !raw.startsWith("git@")) {
54
- return {
55
- source: raw.slice(0, atIdx),
56
- skill: raw.slice(atIdx + 1),
57
- };
71
+ return { source: raw.slice(0, atIdx), skill: raw.slice(atIdx + 1) };
58
72
  }
59
73
  return { source: raw, skill: null };
60
74
  }
61
75
 
62
76
  // ─────────────────────────────────────────────────────────────────
63
- // Install npx skills add <source> [-y] [--global] [--skill name]
77
+ // After npx install: copy new SKILL.md files ~/.agents/<slug>/
78
+ // ─────────────────────────────────────────────────────────────────
79
+ function consolidate(cwd, beforeSnapshot) {
80
+ const copied = [];
81
+ const dest = AGENTS_DIR;
82
+
83
+ // 1. Check known agent dirs first
84
+ for (const rel of KNOWN_AGENT_DIRS) {
85
+ for (const f of scanForSkillMds(path.join(cwd, rel), 3)) {
86
+ if (beforeSnapshot.has(f)) continue;
87
+ const slug = path.basename(path.dirname(f));
88
+ const dir = path.join(dest, slug);
89
+ fs.mkdirSync(dir, { recursive: true });
90
+ fs.copyFileSync(f, path.join(dir, "SKILL.md"));
91
+ copied.push(slug);
92
+ }
93
+ }
94
+
95
+ // 2. Broader scan of cwd for anything missed
96
+ if (copied.length === 0) {
97
+ for (const f of scanForSkillMds(cwd, 5)) {
98
+ if (beforeSnapshot.has(f)) continue;
99
+ // Skip anything already inside ~/.agents
100
+ if (f.startsWith(dest)) continue;
101
+ const slug = path.basename(path.dirname(f));
102
+ const dir = path.join(dest, slug);
103
+ fs.mkdirSync(dir, { recursive: true });
104
+ fs.copyFileSync(f, path.join(dir, "SKILL.md"));
105
+ copied.push(slug);
106
+ }
107
+ }
108
+
109
+ return [...new Set(copied)]; // deduplicate
110
+ }
111
+
112
+ // ─────────────────────────────────────────────────────────────────
113
+ // Install — npx skills puts files directly into ~/.agents/skills/
114
+ // No post-copy needed; just run npx and report what's new
64
115
  // ─────────────────────────────────────────────────────────────────
65
116
  export async function installSkill(rawSource, opts = {}) {
66
- const { global = false, skill: explicitSkill = null, all = false } = opts;
117
+ const { skill: explicitSkill = null, all = false } = opts;
67
118
  const { source, skill: parsedSkill } = parseSource(rawSource);
68
119
  const skillName = explicitSkill ?? parsedSkill;
69
120
 
70
- // Build args — let npx skills auto-detect agent (more reliable than -a gemini)
121
+ // Snapshot before
122
+ const before = new Set(loadSkills().map(s => s.slug));
123
+
71
124
  const parts = ["add", source, "-y"];
72
- if (global) parts.push("-g");
73
125
  if (skillName) parts.push(`--skill "${skillName}"`);
74
126
  if (all) parts.push("--skill '*'");
75
127
 
76
128
  const { stdout, stderr } = await npxSkills(parts.join(" "));
77
- return { output: stdout || stderr };
129
+
130
+ // Report what was newly installed
131
+ const after = loadSkills().map(s => s.slug);
132
+ const newSlugs = after.filter(s => !before.has(s));
133
+
134
+ return { output: stdout || stderr, installed: newSlugs };
78
135
  }
79
136
 
80
137
  // ─────────────────────────────────────────────────────────────────
81
- // List installed npx skills list
138
+ // Removechecks both ~/.agents/<slug>/ and ~/.agents/skills/<slug>/
82
139
  // ─────────────────────────────────────────────────────────────────
83
- export async function listNpxSkills(global = false) {
84
- const args = global ? "list -g" : "list";
85
- const { stdout } = await npxSkills(args);
86
- return stdout;
140
+ export async function removeSkillNpx(slug) {
141
+ const candidates = [
142
+ path.join(AGENTS_DIR, slug),
143
+ path.join(AGENTS_DIR, "skills", slug),
144
+ ];
145
+ const found = candidates.filter(d => fs.existsSync(d));
146
+ if (!found.length) throw new Error(`"${slug}" not found in ~/.agents/`);
147
+
148
+ found.forEach(d => fs.rmSync(d, { recursive: true, force: true }));
149
+ try { await npxSkills(`remove ${slug} -y`); } catch {}
150
+
151
+ return { output: `Removed: ${found.join(", ")}` };
87
152
  }
88
153
 
89
154
  // ─────────────────────────────────────────────────────────────────
90
- // Search npx skills find <query>
155
+ // Delegate to npx for search / list / update / init
91
156
  // ─────────────────────────────────────────────────────────────────
92
157
  export async function findSkills(query) {
93
158
  const { stdout } = await npxSkills(`find ${JSON.stringify(query)}`);
94
159
  return stdout;
95
160
  }
96
-
97
- // ─────────────────────────────────────────────────────────────────
98
- // Remove — npx skills remove <slug>
99
- // ─────────────────────────────────────────────────────────────────
100
- export async function removeSkillNpx(slug) {
101
- const { stdout, stderr } = await npxSkills(`remove ${slug} -y`);
102
- return { output: stdout || stderr };
161
+ export async function listNpxSkills() {
162
+ const { stdout } = await npxSkills("list");
163
+ return stdout;
103
164
  }
104
-
105
- // ─────────────────────────────────────────────────────────────────
106
- // Update — npx skills update [slug]
107
- // ─────────────────────────────────────────────────────────────────
108
165
  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 };
166
+ const { stdout } = await npxSkills(slug ? `update ${slug} -y` : "update -y");
167
+ return stdout;
112
168
  }
113
-
114
- // ─────────────────────────────────────────────────────────────────
115
- // Init — npx skills init [name] (creates a SKILL.md template)
116
- // ─────────────────────────────────────────────────────────────────
117
169
  export async function initSkill(name = "") {
118
- const args = name ? `init "${name}"` : "init";
119
- const { stdout, stderr } = await npxSkills(args);
120
- return { output: stdout || stderr };
170
+ const { stdout } = await npxSkills(name ? `init "${name}"` : "init");
171
+ return stdout;
121
172
  }
122
173
 
123
174
  // ─────────────────────────────────────────────────────────────────
124
- // Load skills for agent context injection
125
- // Scans all skill directories and reads SKILL.md files
175
+ // Load all skills scans both:
176
+ // ~/.agents/<slug>/SKILL.md (manual / flat)
177
+ // ~/.agents/skills/<slug>/SKILL.md (npx skills default)
126
178
  // ─────────────────────────────────────────────────────────────────
127
179
  export function loadSkills() {
128
180
  const skills = [];
129
181
  const seen = new Set();
130
182
 
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) {
183
+ const scanBase = (baseDir) => {
184
+ if (!fs.existsSync(baseDir)) return;
185
+ for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) {
136
186
  if (!entry.isDirectory()) continue;
137
-
138
- const skillDir = path.join(dir, entry.name);
139
- const skillMd = path.join(skillDir, "SKILL.md");
187
+ if (seen.has(entry.name)) continue;
188
+ const skillMd = path.join(baseDir, entry.name, "SKILL.md");
140
189
  if (!fs.existsSync(skillMd)) continue;
141
-
142
- const key = entry.name;
143
- if (seen.has(key)) continue; // project takes priority over global
144
- seen.add(key);
145
-
146
- const content = fs.readFileSync(skillMd, "utf8");
147
-
148
- // Parse optional frontmatter name from SKILL.md
190
+ seen.add(entry.name);
191
+ const content = fs.readFileSync(skillMd, "utf8");
149
192
  const nameMatch = content.match(/^#\s+(.+)$/m);
150
- const name = nameMatch?.[1]?.trim() ?? entry.name;
151
-
152
- skills.push({ name, slug: entry.name, content, scope, path: skillDir });
193
+ skills.push({
194
+ name: nameMatch?.[1]?.trim() ?? entry.name,
195
+ slug: entry.name,
196
+ content,
197
+ path: path.join(baseDir, entry.name),
198
+ });
153
199
  }
154
200
  };
155
201
 
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");
202
+ // Flat: ~/.agents/<slug>/SKILL.md
203
+ scanBase(AGENTS_DIR);
204
+ // Nested: ~/.agents/skills/<slug>/SKILL.md (npx skills default)
205
+ scanBase(path.join(AGENTS_DIR, "skills"));
160
206
 
161
207
  return skills;
162
208
  }
163
209
 
164
- // ─────────────────────────────────────────────────────────────────
165
- // Build skills section injected into agent system prompt
166
- // ─────────────────────────────────────────────────────────────────
167
210
  export function buildSkillsPrompt(skills) {
168
211
  if (!skills.length) return null;
169
-
170
212
  const sections = skills.map(s =>
171
213
  `### Skill: ${s.name}\n${s.content.trim()}`
172
214
  ).join("\n\n---\n\n");
173
-
174
- return `## INSTALLED SKILLS\n\nApply the following skills when relevant to the current task:\n\n${sections}`;
215
+ return `## INSTALLED SKILLS\n\nApply the following skills when relevant:\n\n${sections}`;
175
216
  }
176
217
 
177
- // ─────────────────────────────────────────────────────────────────
178
- // Quick disk check — list installed skills without running npx
179
- // ─────────────────────────────────────────────────────────────────
180
218
  export function listInstalledSkills() {
181
- const result = [];
182
-
183
- const scanDir = (dir, isGlobal) => {
184
- if (!fs.existsSync(dir)) return;
185
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
186
- if (!entry.isDirectory()) continue;
187
- const skillDir = path.join(dir, entry.name);
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);
191
- result.push({
192
- slug: entry.name,
193
- name: nameMatch?.[1]?.trim() ?? entry.name,
194
- global: isGlobal,
195
- dir: skillDir,
196
- });
197
- }
198
- };
199
-
200
- scanDir(PROJECT_SKILLS_DIR(), false);
201
- scanDir(CUSTOM_SKILLS_DIR(), false);
202
- scanDir(GLOBAL_SKILLS_DIR, true);
203
- return result;
219
+ return loadSkills().map(s => ({ ...s, global: true, dir: s.path }));
204
220
  }