@ikyyofc/gemini-cli 3.0.3 → 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 (3) hide show
  1. package/index.js +6 -3
  2. package/package.json +1 -1
  3. package/src/skills.js +144 -130
package/index.js CHANGED
@@ -279,12 +279,15 @@ async function handleCommand(input) {
279
279
  try {
280
280
  const { output } = await installSkill(source, { global: isGlobal, skill: skillName, all });
281
281
  sp.stop();
282
- // Print npx output
283
282
  if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
284
283
  printSuccess("skill installed — active on next message");
285
284
  } catch (e) {
286
- sp.fail(e.message.split("\n")[0]);
287
- printError(e.message.split("\n")[0]);
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");
288
291
  }
289
292
  break;
290
293
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikyyofc/gemini-cli",
3
- "version": "3.0.3",
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,176 +8,196 @@ 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
- 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() };
52
+ try {
53
+ const { stdout, stderr } = await execAsync(cmd, {
54
+ cwd, timeout,
55
+ env: { ...process.env, NO_COLOR: "1", CI: "1" },
56
+ });
57
+ return { stdout: stdout.trim(), stderr: stderr.trim() };
58
+ } catch (err) {
59
+ const detail = [err.stdout?.trim(), err.stderr?.trim()]
60
+ .filter(Boolean).join("\n") || err.message;
61
+ throw new Error(detail);
62
+ }
44
63
  }
45
64
 
46
65
  // ─────────────────────────────────────────────────────────────────
47
- // Install — npx skills add <source> -a gemini -y [--global]
66
+ // Parse "owner/repo@skill-name" shorthand
48
67
  // ─────────────────────────────────────────────────────────────────
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;
55
-
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 '*'";
60
-
61
- const { stdout, stderr } = await npxSkills(args);
62
- return { output: stdout || stderr };
68
+ function parseSource(raw) {
69
+ const atIdx = raw.indexOf("@");
70
+ if (atIdx > 0 && !raw.startsWith("http") && !raw.startsWith("git@")) {
71
+ return { source: raw.slice(0, atIdx), skill: raw.slice(atIdx + 1) };
72
+ }
73
+ return { source: raw, skill: null };
63
74
  }
64
75
 
65
76
  // ─────────────────────────────────────────────────────────────────
66
- // List installed npx skills list
77
+ // After npx install: copy new SKILL.md files → ~/.agents/<slug>/
67
78
  // ─────────────────────────────────────────────────────────────────
68
- export async function listNpxSkills(global = false) {
69
- const args = global ? "list -g" : "list";
70
- const { stdout } = await npxSkills(args);
71
- return stdout;
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
72
110
  }
73
111
 
74
112
  // ─────────────────────────────────────────────────────────────────
75
- // Search — npx skills find <query>
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/
76
117
  // ─────────────────────────────────────────────────────────────────
77
- export async function findSkills(query) {
78
- const { stdout } = await npxSkills(`find ${JSON.stringify(query)}`);
79
- return stdout;
118
+ export async function installSkill(rawSource, opts = {}) {
119
+ const { skill: explicitSkill = null, all = false } = opts;
120
+ const { source, skill: parsedSkill } = parseSource(rawSource);
121
+ const skillName = explicitSkill ?? parsedSkill;
122
+
123
+ const cwd = process.cwd();
124
+ const before = new Set(
125
+ KNOWN_AGENT_DIRS.flatMap(rel => scanForSkillMds(path.join(cwd, rel), 3))
126
+ );
127
+
128
+ const parts = ["add", source, "-y"];
129
+ if (skillName) parts.push(`--skill "${skillName}"`);
130
+ if (all) parts.push("--skill '*'");
131
+
132
+ const { stdout, stderr } = await npxSkills(parts.join(" "));
133
+ const installed = consolidate(cwd, before);
134
+
135
+ return { output: stdout || stderr, installed };
80
136
  }
81
137
 
82
138
  // ─────────────────────────────────────────────────────────────────
83
- // Remove — npx skills remove <slug>
139
+ // Remove — delete from ~/.agents/ then best-effort npx remove
84
140
  // ─────────────────────────────────────────────────────────────────
85
141
  export async function removeSkillNpx(slug) {
86
- const { stdout, stderr } = await npxSkills(`remove ${slug} -y`);
87
- return { output: stdout || stderr };
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}` };
88
147
  }
89
148
 
90
149
  // ─────────────────────────────────────────────────────────────────
91
- // Update npx skills update [slug]
150
+ // Delegate to npx for search / list / update / init
92
151
  // ─────────────────────────────────────────────────────────────────
152
+ export async function findSkills(query) {
153
+ const { stdout } = await npxSkills(`find ${JSON.stringify(query)}`);
154
+ return stdout;
155
+ }
156
+ export async function listNpxSkills() {
157
+ const { stdout } = await npxSkills("list");
158
+ return stdout;
159
+ }
93
160
  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 };
161
+ const { stdout } = await npxSkills(slug ? `update ${slug} -y` : "update -y");
162
+ return stdout;
97
163
  }
98
-
99
- // ─────────────────────────────────────────────────────────────────
100
- // Init — npx skills init [name] (creates a SKILL.md template)
101
- // ─────────────────────────────────────────────────────────────────
102
164
  export async function initSkill(name = "") {
103
- const args = name ? `init "${name}"` : "init";
104
- const { stdout, stderr } = await npxSkills(args);
105
- return { output: stdout || stderr };
165
+ const { stdout } = await npxSkills(name ? `init "${name}"` : "init");
166
+ return stdout;
106
167
  }
107
168
 
108
169
  // ─────────────────────────────────────────────────────────────────
109
- // Load skills for agent context injection
110
- // Scans all skill directories and reads SKILL.md files
170
+ // Load all skills from ~/.agents/ for agent context injection
111
171
  // ─────────────────────────────────────────────────────────────────
112
172
  export function loadSkills() {
113
173
  const skills = [];
114
- const seen = new Set();
115
-
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;
122
-
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 });
138
- }
139
- };
140
-
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
-
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
+ }
146
190
  return skills;
147
191
  }
148
192
 
149
- // ─────────────────────────────────────────────────────────────────
150
- // Build skills section injected into agent system prompt
151
- // ─────────────────────────────────────────────────────────────────
152
193
  export function buildSkillsPrompt(skills) {
153
194
  if (!skills.length) return null;
154
-
155
195
  const sections = skills.map(s =>
156
196
  `### Skill: ${s.name}\n${s.content.trim()}`
157
197
  ).join("\n\n---\n\n");
158
-
159
- 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}`;
160
199
  }
161
200
 
162
- // ─────────────────────────────────────────────────────────────────
163
- // Quick disk check — list installed skills without running npx
164
- // ─────────────────────────────────────────────────────────────────
165
201
  export function listInstalledSkills() {
166
- const result = [];
167
-
168
- const scanDir = (dir, isGlobal) => {
169
- if (!fs.existsSync(dir)) return;
170
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
171
- if (!entry.isDirectory()) continue;
172
- const skillDir = path.join(dir, entry.name);
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);
176
- result.push({
177
- slug: entry.name,
178
- name: nameMatch?.[1]?.trim() ?? entry.name,
179
- global: isGlobal,
180
- dir: skillDir,
181
- });
182
- }
183
- };
184
-
185
- scanDir(PROJECT_SKILLS_DIR(), false);
186
- scanDir(CUSTOM_SKILLS_DIR(), false);
187
- scanDir(GLOBAL_SKILLS_DIR, true);
188
- return result;
202
+ return loadSkills().map(s => ({ ...s, global: true, dir: s.path }));
189
203
  }