@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.
- package/index.js +6 -3
- package/package.json +1 -1
- 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.
|
|
287
|
-
|
|
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
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,176 +8,196 @@ 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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
//
|
|
66
|
+
// Parse "owner/repo@skill-name" shorthand
|
|
48
67
|
// ─────────────────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
//
|
|
77
|
+
// After npx install: copy new SKILL.md files → ~/.agents/<slug>/
|
|
67
78
|
// ─────────────────────────────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
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
|
-
//
|
|
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
|
|
78
|
-
const {
|
|
79
|
-
|
|
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
|
|
139
|
+
// Remove — delete from ~/.agents/ then best-effort npx remove
|
|
84
140
|
// ─────────────────────────────────────────────────────────────────
|
|
85
141
|
export async function removeSkillNpx(slug) {
|
|
86
|
-
const
|
|
87
|
-
|
|
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
|
-
//
|
|
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
|
|
95
|
-
|
|
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
|
|
104
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
if (!
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
}
|