@ikyyofc/gemini-cli 3.0.4 → 3.0.5

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 +120 -121
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.5",
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,141 @@ 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
114
+ // 1. Snapshot existing SKILL.md paths
115
+ // 2. npx skills add (wherever it installs)
116
+ // 3. Find new SKILL.md files and copy ALL → ~/.agents/
64
117
  // ─────────────────────────────────────────────────────────────────
65
118
  export async function installSkill(rawSource, opts = {}) {
66
- const { global = false, skill: explicitSkill = null, all = false } = opts;
119
+ const { skill: explicitSkill = null, all = false } = opts;
67
120
  const { source, skill: parsedSkill } = parseSource(rawSource);
68
121
  const skillName = explicitSkill ?? parsedSkill;
69
122
 
70
- // Build args — let npx skills auto-detect agent (more reliable than -a gemini)
123
+ const cwd = process.cwd();
124
+ const before = new Set(
125
+ KNOWN_AGENT_DIRS.flatMap(rel => scanForSkillMds(path.join(cwd, rel), 3))
126
+ );
127
+
71
128
  const parts = ["add", source, "-y"];
72
- if (global) parts.push("-g");
73
129
  if (skillName) parts.push(`--skill "${skillName}"`);
74
130
  if (all) parts.push("--skill '*'");
75
131
 
76
132
  const { stdout, stderr } = await npxSkills(parts.join(" "));
77
- return { output: stdout || stderr };
133
+ const installed = consolidate(cwd, before);
134
+
135
+ return { output: stdout || stderr, installed };
78
136
  }
79
137
 
80
138
  // ─────────────────────────────────────────────────────────────────
81
- // List installed — npx skills list
139
+ // Removedelete from ~/.agents/ then best-effort npx remove
82
140
  // ─────────────────────────────────────────────────────────────────
83
- export async function listNpxSkills(global = false) {
84
- const args = global ? "list -g" : "list";
85
- const { stdout } = await npxSkills(args);
86
- return stdout;
141
+ export async function removeSkillNpx(slug) {
142
+ const skillDir = path.join(AGENTS_DIR, slug);
143
+ if (!fs.existsSync(skillDir)) throw new Error(`"${slug}" not found in ~/.agents/`);
144
+ fs.rmSync(skillDir, { recursive: true, force: true });
145
+ try { await npxSkills(`remove ${slug} -y`); } catch {}
146
+ return { output: `Removed ~/.agents/${slug}` };
87
147
  }
88
148
 
89
149
  // ─────────────────────────────────────────────────────────────────
90
- // Search npx skills find <query>
150
+ // Delegate to npx for search / list / update / init
91
151
  // ─────────────────────────────────────────────────────────────────
92
152
  export async function findSkills(query) {
93
153
  const { stdout } = await npxSkills(`find ${JSON.stringify(query)}`);
94
154
  return stdout;
95
155
  }
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 };
156
+ export async function listNpxSkills() {
157
+ const { stdout } = await npxSkills("list");
158
+ return stdout;
103
159
  }
104
-
105
- // ─────────────────────────────────────────────────────────────────
106
- // Update — npx skills update [slug]
107
- // ─────────────────────────────────────────────────────────────────
108
160
  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 };
161
+ const { stdout } = await npxSkills(slug ? `update ${slug} -y` : "update -y");
162
+ return stdout;
112
163
  }
113
-
114
- // ─────────────────────────────────────────────────────────────────
115
- // Init — npx skills init [name] (creates a SKILL.md template)
116
- // ─────────────────────────────────────────────────────────────────
117
164
  export async function initSkill(name = "") {
118
- const args = name ? `init "${name}"` : "init";
119
- const { stdout, stderr } = await npxSkills(args);
120
- return { output: stdout || stderr };
165
+ const { stdout } = await npxSkills(name ? `init "${name}"` : "init");
166
+ return stdout;
121
167
  }
122
168
 
123
169
  // ─────────────────────────────────────────────────────────────────
124
- // Load skills for agent context injection
125
- // Scans all skill directories and reads SKILL.md files
170
+ // Load all skills from ~/.agents/ for agent context injection
126
171
  // ─────────────────────────────────────────────────────────────────
127
172
  export function loadSkills() {
128
173
  const skills = [];
129
- const seen = new Set();
130
-
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;
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
149
- 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 });
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
-
174
+ if (!fs.existsSync(AGENTS_DIR)) return skills;
175
+
176
+ for (const entry of fs.readdirSync(AGENTS_DIR, { withFileTypes: true })) {
177
+ if (!entry.isDirectory()) continue;
178
+ const skillMd = path.join(AGENTS_DIR, entry.name, "SKILL.md");
179
+ if (!fs.existsSync(skillMd)) continue;
180
+
181
+ const content = fs.readFileSync(skillMd, "utf8");
182
+ const nameMatch = content.match(/^#\s+(.+)$/m);
183
+ skills.push({
184
+ name: nameMatch?.[1]?.trim() ?? entry.name,
185
+ slug: entry.name,
186
+ content,
187
+ path: path.join(AGENTS_DIR, entry.name),
188
+ });
189
+ }
161
190
  return skills;
162
191
  }
163
192
 
164
- // ─────────────────────────────────────────────────────────────────
165
- // Build skills section injected into agent system prompt
166
- // ─────────────────────────────────────────────────────────────────
167
193
  export function buildSkillsPrompt(skills) {
168
194
  if (!skills.length) return null;
169
-
170
195
  const sections = skills.map(s =>
171
196
  `### Skill: ${s.name}\n${s.content.trim()}`
172
197
  ).join("\n\n---\n\n");
173
-
174
- return `## INSTALLED SKILLS\n\nApply the following skills when relevant to the current task:\n\n${sections}`;
198
+ return `## INSTALLED SKILLS\n\nApply the following skills when relevant:\n\n${sections}`;
175
199
  }
176
200
 
177
- // ─────────────────────────────────────────────────────────────────
178
- // Quick disk check — list installed skills without running npx
179
- // ─────────────────────────────────────────────────────────────────
180
201
  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;
202
+ return loadSkills().map(s => ({ ...s, global: true, dir: s.path }));
204
203
  }