@ikyyofc/gemini-cli 3.0.2 → 3.0.4
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 +82 -80
- package/package.json +1 -1
- package/src/skills.js +136 -150
package/index.js
CHANGED
|
@@ -25,8 +25,9 @@ import {
|
|
|
25
25
|
import { setupGlobalProxy, proxyStatus, setProxyEnabled } from "./src/utils/proxy.js";
|
|
26
26
|
import { Spinner } from "./src/utils/spinner.js";
|
|
27
27
|
import {
|
|
28
|
-
listInstalledSkills, installSkill,
|
|
29
|
-
|
|
28
|
+
listInstalledSkills, installSkill, removeSkillNpx,
|
|
29
|
+
findSkills, listNpxSkills, updateSkill, initSkill,
|
|
30
|
+
ensureSkillsDirs, loadSkills,
|
|
30
31
|
} from "./src/skills.js";
|
|
31
32
|
|
|
32
33
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -240,131 +241,132 @@ async function handleCommand(input) {
|
|
|
240
241
|
const sub = tokens[1]?.toLowerCase();
|
|
241
242
|
const rest = tokens.slice(2).join(" ").trim();
|
|
242
243
|
|
|
243
|
-
// /skill list
|
|
244
|
-
if (!sub || sub === "list") {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
printInfo("
|
|
244
|
+
// ── /skill list ──────────────────────────────────────
|
|
245
|
+
if (!sub || sub === "list" || sub === "ls") {
|
|
246
|
+
// Quick disk scan (instant, no npx)
|
|
247
|
+
const disk = listInstalledSkills();
|
|
248
|
+
if (!disk.length) {
|
|
249
|
+
printInfo("no skills installed");
|
|
250
|
+
printInfo("browse: https://skills.sh · install: /skill add <owner/repo>");
|
|
249
251
|
break;
|
|
250
252
|
}
|
|
251
253
|
console.log("");
|
|
252
|
-
|
|
254
|
+
disk.forEach(s => {
|
|
253
255
|
const scope = s.global ? chalk.dim("global") : chalk.hex("#4EC9B0")("project");
|
|
254
256
|
console.log(
|
|
255
|
-
" " + chalk.hex("#FFD080").bold(s.
|
|
256
|
-
chalk.dim(s.
|
|
257
|
+
" " + chalk.hex("#FFD080").bold(s.name.padEnd(30)) +
|
|
258
|
+
chalk.dim(s.slug.padEnd(26)) + scope
|
|
257
259
|
);
|
|
258
260
|
});
|
|
259
|
-
console.log(chalk.dim(`\n ${
|
|
261
|
+
console.log(chalk.dim(`\n ${disk.length} skill(s) active in agent context\n`));
|
|
260
262
|
break;
|
|
261
263
|
}
|
|
262
264
|
|
|
263
|
-
// /skill add <
|
|
265
|
+
// ── /skill add <source> [--global] [--skill <name>] ──
|
|
264
266
|
if (sub === "add" || sub === "install") {
|
|
265
|
-
const isGlobal
|
|
266
|
-
const
|
|
267
|
-
|
|
267
|
+
const isGlobal = rest.includes("--global") || rest.includes("-g");
|
|
268
|
+
const skillMatch = rest.match(/--skill\s+"?([^"]+)"?/);
|
|
269
|
+
const skillName = skillMatch?.[1] ?? null;
|
|
270
|
+
const all = rest.includes("--all");
|
|
271
|
+
const source = rest
|
|
272
|
+
.replace(/--global|-g|--skill\s+"?[^"]*"?|--all/g, "")
|
|
273
|
+
.trim();
|
|
274
|
+
|
|
275
|
+
if (!source) { printError("usage: /skill add <owner/repo> [--global] [--skill <name>] [--all]"); break; }
|
|
276
|
+
|
|
268
277
|
const sp = new Spinner();
|
|
269
|
-
sp.start(`
|
|
278
|
+
sp.start(`npx skills add ${source}…`, "#FFD080");
|
|
270
279
|
try {
|
|
271
|
-
const
|
|
272
|
-
sp.
|
|
273
|
-
|
|
280
|
+
const { output } = await installSkill(source, { global: isGlobal, skill: skillName, all });
|
|
281
|
+
sp.stop();
|
|
282
|
+
if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
|
|
283
|
+
printSuccess("skill installed — active on next message");
|
|
274
284
|
} catch (e) {
|
|
275
|
-
sp.
|
|
276
|
-
|
|
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");
|
|
277
291
|
}
|
|
278
292
|
break;
|
|
279
293
|
}
|
|
280
294
|
|
|
281
|
-
// /skill remove <slug>
|
|
295
|
+
// ── /skill remove <slug> ──────────────────────────────
|
|
282
296
|
if (sub === "remove" || sub === "rm") {
|
|
283
|
-
|
|
284
|
-
|
|
297
|
+
if (!rest) { printError("usage: /skill remove <slug>"); break; }
|
|
298
|
+
const sp = new Spinner();
|
|
299
|
+
sp.start(`removing ${rest}…`, "#FF9060");
|
|
285
300
|
try {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
301
|
+
const { output } = await removeSkillNpx(rest);
|
|
302
|
+
sp.stop();
|
|
303
|
+
if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
|
|
304
|
+
printSuccess(`removed: ${rest}`);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
sp.fail(e.message.split("\n")[0]);
|
|
307
|
+
printError(e.message.split("\n")[0]);
|
|
308
|
+
}
|
|
289
309
|
break;
|
|
290
310
|
}
|
|
291
311
|
|
|
292
|
-
// /skill search <query>
|
|
293
|
-
if (sub === "search") {
|
|
294
|
-
if (!rest) { printError("usage: /skill
|
|
312
|
+
// ── /skill find / search <query> ─────────────────────
|
|
313
|
+
if (sub === "find" || sub === "search") {
|
|
314
|
+
if (!rest) { printError("usage: /skill find <query>"); break; }
|
|
295
315
|
const sp = new Spinner();
|
|
296
316
|
sp.start(`searching "${rest}"…`, "#4A9EFF");
|
|
297
317
|
try {
|
|
298
|
-
const
|
|
318
|
+
const output = await findSkills(rest);
|
|
299
319
|
sp.stop();
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
console.log(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (r.name !== r.slug)
|
|
308
|
-
console.log(" " + chalk.dim(" ".repeat(2) + r.name));
|
|
309
|
-
});
|
|
310
|
-
console.log(chalk.dim("\n install: /skill add <id>\n"));
|
|
320
|
+
if (output) {
|
|
321
|
+
console.log("");
|
|
322
|
+
output.split("\n").forEach(l => console.log(" " + l));
|
|
323
|
+
console.log("");
|
|
324
|
+
} else {
|
|
325
|
+
printInfo("no results — try a different query");
|
|
326
|
+
}
|
|
311
327
|
} catch (e) {
|
|
312
|
-
sp.fail(e.message);
|
|
313
|
-
printError(e.message);
|
|
328
|
+
sp.fail(e.message.split("\n")[0]);
|
|
329
|
+
printError(e.message.split("\n")[0]);
|
|
314
330
|
}
|
|
315
331
|
break;
|
|
316
332
|
}
|
|
317
333
|
|
|
318
|
-
// /skill
|
|
319
|
-
if (sub === "
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
sp.start(`fetching ${view}…`, "#4A9EFF");
|
|
334
|
+
// ── /skill update [slug] ──────────────────────────────
|
|
335
|
+
if (sub === "update") {
|
|
336
|
+
const sp = new Spinner();
|
|
337
|
+
sp.start(rest ? `updating ${rest}…` : "updating all skills…", "#4A9EFF");
|
|
323
338
|
try {
|
|
324
|
-
const
|
|
339
|
+
const { output } = await updateSkill(rest);
|
|
325
340
|
sp.stop();
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
console.log(
|
|
329
|
-
" " + chalk.dim(String(i+1).padStart(2) + ".") + " " +
|
|
330
|
-
chalk.hex("#4A9EFF").bold(r.id.padEnd(46)) +
|
|
331
|
-
chalk.dim(String(r.installs).padStart(8) + " installs")
|
|
332
|
-
);
|
|
333
|
-
});
|
|
334
|
-
console.log(chalk.dim("\n /skill browse trending · /skill browse hot\n"));
|
|
341
|
+
if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
|
|
342
|
+
printSuccess("updated");
|
|
335
343
|
} catch (e) {
|
|
336
|
-
sp.fail(e.message);
|
|
337
|
-
printError(e.message);
|
|
344
|
+
sp.fail(e.message.split("\n")[0]);
|
|
345
|
+
printError(e.message.split("\n")[0]);
|
|
338
346
|
}
|
|
339
347
|
break;
|
|
340
348
|
}
|
|
341
349
|
|
|
342
|
-
// /skill
|
|
343
|
-
if (sub === "
|
|
344
|
-
if (!rest) { printError("usage: /skill info <id>"); break; }
|
|
350
|
+
// ── /skill init [name] ────────────────────────────────
|
|
351
|
+
if (sub === "init") {
|
|
345
352
|
const sp = new Spinner();
|
|
346
|
-
sp.start(
|
|
353
|
+
sp.start("creating SKILL.md template…", "#4A9EFF");
|
|
347
354
|
try {
|
|
348
|
-
const {
|
|
349
|
-
const { data } = await ax.get(`https://skills.sh/api/v1/skills/${rest}`);
|
|
355
|
+
const { output } = await initSkill(rest);
|
|
350
356
|
sp.stop();
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
printInfo(`source ${data.source}`);
|
|
354
|
-
printInfo(`installs ${data.installs}`);
|
|
355
|
-
printInfo(`files ${data.files?.length ?? 0}`);
|
|
356
|
-
if (data.files?.length) {
|
|
357
|
-
data.files.forEach(f => console.log(chalk.dim(" " + f.path)));
|
|
358
|
-
}
|
|
359
|
-
console.log(chalk.dim(`\n /skill add ${data.id}\n`));
|
|
357
|
+
if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
|
|
358
|
+
printSuccess("SKILL.md created");
|
|
360
359
|
} catch (e) {
|
|
361
|
-
sp.fail(e.message);
|
|
362
|
-
printError(e.message);
|
|
360
|
+
sp.fail(e.message.split("\n")[0]);
|
|
361
|
+
printError(e.message.split("\n")[0]);
|
|
363
362
|
}
|
|
364
363
|
break;
|
|
365
364
|
}
|
|
366
365
|
|
|
367
|
-
printInfo("usage: /skill [list | add | remove |
|
|
366
|
+
printInfo("usage: /skill [list | add | remove | find | update | init]");
|
|
367
|
+
printInfo(" /skill add vercel-labs/agent-skills");
|
|
368
|
+
printInfo(" /skill add anthropics/skills --skill frontend-design");
|
|
369
|
+
printInfo(" /skill add owner/repo --global");
|
|
368
370
|
break;
|
|
369
371
|
}
|
|
370
372
|
|
package/package.json
CHANGED
package/src/skills.js
CHANGED
|
@@ -1,218 +1,204 @@
|
|
|
1
|
-
// src/skills.js —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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)
|
|
4
9
|
import fs from "fs";
|
|
5
10
|
import path from "path";
|
|
6
11
|
import os from "os";
|
|
7
|
-
import
|
|
12
|
+
import { promisify } from "util";
|
|
13
|
+
import { exec } from "child_process";
|
|
8
14
|
|
|
9
|
-
const
|
|
10
|
-
const GLOBAL_AGENTS_DIR = path.join(os.homedir(), ".gemini", "agents");
|
|
15
|
+
const execAsync = promisify(exec);
|
|
11
16
|
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export function getSkillsDirs() {
|
|
17
|
-
const dirs = [];
|
|
18
|
-
const local = path.join(process.cwd(), ".agents");
|
|
19
|
-
if (fs.existsSync(local)) dirs.push(local);
|
|
20
|
-
if (fs.existsSync(GLOBAL_AGENTS_DIR)) dirs.push(GLOBAL_AGENTS_DIR);
|
|
21
|
-
return dirs;
|
|
22
|
-
}
|
|
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");
|
|
23
21
|
|
|
24
22
|
export function ensureSkillsDirs() {
|
|
25
|
-
fs.mkdirSync(
|
|
23
|
+
fs.mkdirSync(GLOBAL_SKILLS_DIR, { recursive: true });
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
// ─────────────────────────────────────────────────────────────────
|
|
29
|
-
//
|
|
30
|
-
// ─────────────────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (seen.has(entry.name)) continue; // project-level takes priority
|
|
46
|
-
seen.add(entry.name);
|
|
47
|
-
|
|
48
|
-
let meta = {};
|
|
49
|
-
try { meta = JSON.parse(fs.readFileSync(metaFile, "utf8")); } catch {}
|
|
50
|
-
|
|
51
|
-
const content = fs.readFileSync(skillMd, "utf8");
|
|
52
|
-
|
|
53
|
-
// Also read any supporting files (examples, etc.)
|
|
54
|
-
const extras = fs.readdirSync(skillDir)
|
|
55
|
-
.filter(f => f !== "SKILL.md" && f !== ".meta.json" && f.endsWith(".md"))
|
|
56
|
-
.map(f => fs.readFileSync(path.join(skillDir, f), "utf8"))
|
|
57
|
-
.join("\n\n");
|
|
58
|
-
|
|
59
|
-
skills.push({
|
|
60
|
-
name: meta.name ?? entry.name,
|
|
61
|
-
slug: meta.slug ?? entry.name,
|
|
62
|
-
source: meta.source ?? "local",
|
|
63
|
-
path: skillDir,
|
|
64
|
-
content: content + (extras ? "\n\n" + extras : ""),
|
|
65
|
-
global: dir === GLOBAL_AGENTS_DIR,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
27
|
+
// Run npx skills with timeout
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────
|
|
29
|
+
async function npxSkills(args, cwd = process.cwd(), timeout = 120_000) {
|
|
30
|
+
const cmd = `npx --yes skills ${args}`;
|
|
31
|
+
try {
|
|
32
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
33
|
+
cwd,
|
|
34
|
+
timeout,
|
|
35
|
+
env: { ...process.env, NO_COLOR: "1", CI: "1" },
|
|
36
|
+
});
|
|
37
|
+
return { stdout: stdout.trim(), stderr: stderr.trim() };
|
|
38
|
+
} catch (err) {
|
|
39
|
+
// Expose actual npx output in the thrown error message
|
|
40
|
+
const detail = [err.stdout?.trim(), err.stderr?.trim()]
|
|
41
|
+
.filter(Boolean).join("\n") || err.message;
|
|
42
|
+
throw new Error(detail);
|
|
68
43
|
}
|
|
44
|
+
}
|
|
69
45
|
|
|
70
|
-
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────
|
|
47
|
+
// Parse source: handles "owner/repo@skill-name" shorthand
|
|
48
|
+
// e.g. "anthropics/skills@frontend-design"
|
|
49
|
+
// → source="anthropics/skills", skill="frontend-design"
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────
|
|
51
|
+
function parseSource(raw) {
|
|
52
|
+
const atIdx = raw.indexOf("@");
|
|
53
|
+
if (atIdx > 0 && !raw.startsWith("http") && !raw.startsWith("git@")) {
|
|
54
|
+
return {
|
|
55
|
+
source: raw.slice(0, atIdx),
|
|
56
|
+
skill: raw.slice(atIdx + 1),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return { source: raw, skill: null };
|
|
71
60
|
}
|
|
72
61
|
|
|
73
62
|
// ─────────────────────────────────────────────────────────────────
|
|
74
|
-
//
|
|
63
|
+
// Install — npx skills add <source> [-y] [--global] [--skill name]
|
|
75
64
|
// ─────────────────────────────────────────────────────────────────
|
|
76
|
-
export function
|
|
77
|
-
|
|
65
|
+
export async function installSkill(rawSource, opts = {}) {
|
|
66
|
+
const { global = false, skill: explicitSkill = null, all = false } = opts;
|
|
67
|
+
const { source, skill: parsedSkill } = parseSource(rawSource);
|
|
68
|
+
const skillName = explicitSkill ?? parsedSkill;
|
|
78
69
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
).
|
|
70
|
+
// Build args — let npx skills auto-detect agent (more reliable than -a gemini)
|
|
71
|
+
const parts = ["add", source, "-y"];
|
|
72
|
+
if (global) parts.push("-g");
|
|
73
|
+
if (skillName) parts.push(`--skill "${skillName}"`);
|
|
74
|
+
if (all) parts.push("--skill '*'");
|
|
75
|
+
|
|
76
|
+
const { stdout, stderr } = await npxSkills(parts.join(" "));
|
|
77
|
+
return { output: stdout || stderr };
|
|
78
|
+
}
|
|
82
79
|
|
|
83
|
-
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────
|
|
81
|
+
// List installed — npx skills list
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────
|
|
83
|
+
export async function listNpxSkills(global = false) {
|
|
84
|
+
const args = global ? "list -g" : "list";
|
|
85
|
+
const { stdout } = await npxSkills(args);
|
|
86
|
+
return stdout;
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
// ─────────────────────────────────────────────────────────────────
|
|
87
|
-
//
|
|
90
|
+
// Search — npx skills find <query>
|
|
88
91
|
// ─────────────────────────────────────────────────────────────────
|
|
89
|
-
async function
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
params,
|
|
93
|
-
headers: { "User-Agent": "gemini-cli/2.0" },
|
|
94
|
-
timeout: 15000,
|
|
95
|
-
});
|
|
96
|
-
return res.data;
|
|
92
|
+
export async function findSkills(query) {
|
|
93
|
+
const { stdout } = await npxSkills(`find ${JSON.stringify(query)}`);
|
|
94
|
+
return stdout;
|
|
97
95
|
}
|
|
98
96
|
|
|
99
97
|
// ─────────────────────────────────────────────────────────────────
|
|
100
|
-
//
|
|
98
|
+
// Remove — npx skills remove <slug>
|
|
101
99
|
// ─────────────────────────────────────────────────────────────────
|
|
102
|
-
export async function
|
|
103
|
-
const
|
|
104
|
-
return
|
|
100
|
+
export async function removeSkillNpx(slug) {
|
|
101
|
+
const { stdout, stderr } = await npxSkills(`remove ${slug} -y`);
|
|
102
|
+
return { output: stdout || stderr };
|
|
105
103
|
}
|
|
106
104
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
// ─────────────────────────────────────────────────────────────────
|
|
106
|
+
// Update — npx skills update [slug]
|
|
107
|
+
// ─────────────────────────────────────────────────────────────────
|
|
108
|
+
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 };
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
// ─────────────────────────────────────────────────────────────────
|
|
113
|
-
//
|
|
114
|
-
// id format: "owner/repo/slug" e.g. "vercel-labs/agent-skills/next-js-development"
|
|
115
|
+
// Init — npx skills init [name] (creates a SKILL.md template)
|
|
115
116
|
// ─────────────────────────────────────────────────────────────────
|
|
116
|
-
export async function
|
|
117
|
-
const
|
|
118
|
-
|
|
117
|
+
export async function initSkill(name = "") {
|
|
118
|
+
const args = name ? `init "${name}"` : "init";
|
|
119
|
+
const { stdout, stderr } = await npxSkills(args);
|
|
120
|
+
return { output: stdout || stderr };
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
// ─────────────────────────────────────────────────────────────────
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
// scope: "project" | "global"
|
|
124
|
+
// Load skills for agent context injection
|
|
125
|
+
// Scans all skill directories and reads SKILL.md files
|
|
125
126
|
// ─────────────────────────────────────────────────────────────────
|
|
126
|
-
export
|
|
127
|
-
const
|
|
127
|
+
export function loadSkills() {
|
|
128
|
+
const skills = [];
|
|
129
|
+
const seen = new Set();
|
|
128
130
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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;
|
|
132
141
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
142
|
+
const key = entry.name;
|
|
143
|
+
if (seen.has(key)) continue; // project takes priority over global
|
|
144
|
+
seen.add(key);
|
|
136
145
|
|
|
137
|
-
|
|
138
|
-
const skillDir = path.join(targetDir, slug);
|
|
139
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
146
|
+
const content = fs.readFileSync(skillMd, "utf8");
|
|
140
147
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
145
|
-
fs.writeFileSync(filePath, file.contents ?? "", "utf8");
|
|
146
|
-
}
|
|
148
|
+
// Parse optional frontmatter name from SKILL.md
|
|
149
|
+
const nameMatch = content.match(/^#\s+(.+)$/m);
|
|
150
|
+
const name = nameMatch?.[1]?.trim() ?? entry.name;
|
|
147
151
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return { slug, name: skill.name ?? slug, dir: skillDir };
|
|
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
|
+
|
|
161
|
+
return skills;
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
// ─────────────────────────────────────────────────────────────────
|
|
163
|
-
//
|
|
165
|
+
// Build skills section injected into agent system prompt
|
|
164
166
|
// ─────────────────────────────────────────────────────────────────
|
|
165
|
-
export function
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const dirs = scope === "global"
|
|
169
|
-
? [GLOBAL_AGENTS_DIR]
|
|
170
|
-
: scope === "project"
|
|
171
|
-
? [path.join(process.cwd(), ".agents")]
|
|
172
|
-
: [path.join(process.cwd(), ".agents"), GLOBAL_AGENTS_DIR];
|
|
167
|
+
export function buildSkillsPrompt(skills) {
|
|
168
|
+
if (!skills.length) return null;
|
|
173
169
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
178
|
-
removed.push(skillDir);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
170
|
+
const sections = skills.map(s =>
|
|
171
|
+
`### Skill: ${s.name}\n${s.content.trim()}`
|
|
172
|
+
).join("\n\n---\n\n");
|
|
181
173
|
|
|
182
|
-
|
|
183
|
-
return removed;
|
|
174
|
+
return `## INSTALLED SKILLS\n\nApply the following skills when relevant to the current task:\n\n${sections}`;
|
|
184
175
|
}
|
|
185
176
|
|
|
186
177
|
// ─────────────────────────────────────────────────────────────────
|
|
187
|
-
//
|
|
178
|
+
// Quick disk check — list installed skills without running npx
|
|
188
179
|
// ─────────────────────────────────────────────────────────────────
|
|
189
180
|
export function listInstalledSkills() {
|
|
190
181
|
const result = [];
|
|
191
182
|
|
|
192
|
-
const
|
|
183
|
+
const scanDir = (dir, isGlobal) => {
|
|
193
184
|
if (!fs.existsSync(dir)) return;
|
|
194
185
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
195
186
|
if (!entry.isDirectory()) continue;
|
|
196
187
|
const skillDir = path.join(dir, entry.name);
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
let meta = {};
|
|
202
|
-
try { meta = JSON.parse(fs.readFileSync(metaFile, "utf8")); } catch {}
|
|
203
|
-
|
|
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);
|
|
204
191
|
result.push({
|
|
205
192
|
slug: entry.name,
|
|
206
|
-
name:
|
|
207
|
-
source: meta.source ?? "local",
|
|
193
|
+
name: nameMatch?.[1]?.trim() ?? entry.name,
|
|
208
194
|
global: isGlobal,
|
|
209
195
|
dir: skillDir,
|
|
210
|
-
installedAt: meta.installedAt ?? null,
|
|
211
196
|
});
|
|
212
197
|
}
|
|
213
198
|
};
|
|
214
199
|
|
|
215
|
-
|
|
216
|
-
|
|
200
|
+
scanDir(PROJECT_SKILLS_DIR(), false);
|
|
201
|
+
scanDir(CUSTOM_SKILLS_DIR(), false);
|
|
202
|
+
scanDir(GLOBAL_SKILLS_DIR, true);
|
|
217
203
|
return result;
|
|
218
204
|
}
|