@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.
- package/package.json +1 -1
- package/src/skills.js +128 -112
package/package.json
CHANGED
package/src/skills.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
// src/skills.js — Skills manager via npx skills CLI
|
|
2
|
-
// All
|
|
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
|
-
//
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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 {
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
138
|
+
// Remove — checks both ~/.agents/<slug>/ and ~/.agents/skills/<slug>/
|
|
82
139
|
// ─────────────────────────────────────────────────────────────────
|
|
83
|
-
export async function
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
110
|
-
|
|
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
|
|
119
|
-
|
|
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
|
|
125
|
-
//
|
|
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
|
|
132
|
-
if (!fs.existsSync(
|
|
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
|
|
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
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
}
|