@ikyyofc/gemini-cli 3.0.2 → 3.0.3
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 +79 -80
- package/package.json +1 -1
- package/src/skills.js +127 -156
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,129 @@ 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
|
+
// Print npx output
|
|
283
|
+
if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
|
|
284
|
+
printSuccess("skill installed — active on next message");
|
|
274
285
|
} catch (e) {
|
|
275
|
-
sp.fail(e.message);
|
|
276
|
-
printError(e.message);
|
|
286
|
+
sp.fail(e.message.split("\n")[0]);
|
|
287
|
+
printError(e.message.split("\n")[0]);
|
|
277
288
|
}
|
|
278
289
|
break;
|
|
279
290
|
}
|
|
280
291
|
|
|
281
|
-
// /skill remove <slug>
|
|
292
|
+
// ── /skill remove <slug> ──────────────────────────────
|
|
282
293
|
if (sub === "remove" || sub === "rm") {
|
|
283
|
-
|
|
284
|
-
|
|
294
|
+
if (!rest) { printError("usage: /skill remove <slug>"); break; }
|
|
295
|
+
const sp = new Spinner();
|
|
296
|
+
sp.start(`removing ${rest}…`, "#FF9060");
|
|
285
297
|
try {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
298
|
+
const { output } = await removeSkillNpx(rest);
|
|
299
|
+
sp.stop();
|
|
300
|
+
if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
|
|
301
|
+
printSuccess(`removed: ${rest}`);
|
|
302
|
+
} catch (e) {
|
|
303
|
+
sp.fail(e.message.split("\n")[0]);
|
|
304
|
+
printError(e.message.split("\n")[0]);
|
|
305
|
+
}
|
|
289
306
|
break;
|
|
290
307
|
}
|
|
291
308
|
|
|
292
|
-
// /skill search <query>
|
|
293
|
-
if (sub === "search") {
|
|
294
|
-
if (!rest) { printError("usage: /skill
|
|
309
|
+
// ── /skill find / search <query> ─────────────────────
|
|
310
|
+
if (sub === "find" || sub === "search") {
|
|
311
|
+
if (!rest) { printError("usage: /skill find <query>"); break; }
|
|
295
312
|
const sp = new Spinner();
|
|
296
313
|
sp.start(`searching "${rest}"…`, "#4A9EFF");
|
|
297
314
|
try {
|
|
298
|
-
const
|
|
315
|
+
const output = await findSkills(rest);
|
|
299
316
|
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"));
|
|
317
|
+
if (output) {
|
|
318
|
+
console.log("");
|
|
319
|
+
output.split("\n").forEach(l => console.log(" " + l));
|
|
320
|
+
console.log("");
|
|
321
|
+
} else {
|
|
322
|
+
printInfo("no results — try a different query");
|
|
323
|
+
}
|
|
311
324
|
} catch (e) {
|
|
312
|
-
sp.fail(e.message);
|
|
313
|
-
printError(e.message);
|
|
325
|
+
sp.fail(e.message.split("\n")[0]);
|
|
326
|
+
printError(e.message.split("\n")[0]);
|
|
314
327
|
}
|
|
315
328
|
break;
|
|
316
329
|
}
|
|
317
330
|
|
|
318
|
-
// /skill
|
|
319
|
-
if (sub === "
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
sp.start(`fetching ${view}…`, "#4A9EFF");
|
|
331
|
+
// ── /skill update [slug] ──────────────────────────────
|
|
332
|
+
if (sub === "update") {
|
|
333
|
+
const sp = new Spinner();
|
|
334
|
+
sp.start(rest ? `updating ${rest}…` : "updating all skills…", "#4A9EFF");
|
|
323
335
|
try {
|
|
324
|
-
const
|
|
336
|
+
const { output } = await updateSkill(rest);
|
|
325
337
|
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"));
|
|
338
|
+
if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
|
|
339
|
+
printSuccess("updated");
|
|
335
340
|
} catch (e) {
|
|
336
|
-
sp.fail(e.message);
|
|
337
|
-
printError(e.message);
|
|
341
|
+
sp.fail(e.message.split("\n")[0]);
|
|
342
|
+
printError(e.message.split("\n")[0]);
|
|
338
343
|
}
|
|
339
344
|
break;
|
|
340
345
|
}
|
|
341
346
|
|
|
342
|
-
// /skill
|
|
343
|
-
if (sub === "
|
|
344
|
-
if (!rest) { printError("usage: /skill info <id>"); break; }
|
|
347
|
+
// ── /skill init [name] ────────────────────────────────
|
|
348
|
+
if (sub === "init") {
|
|
345
349
|
const sp = new Spinner();
|
|
346
|
-
sp.start(
|
|
350
|
+
sp.start("creating SKILL.md template…", "#4A9EFF");
|
|
347
351
|
try {
|
|
348
|
-
const {
|
|
349
|
-
const { data } = await ax.get(`https://skills.sh/api/v1/skills/${rest}`);
|
|
352
|
+
const { output } = await initSkill(rest);
|
|
350
353
|
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`));
|
|
354
|
+
if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
|
|
355
|
+
printSuccess("SKILL.md created");
|
|
360
356
|
} catch (e) {
|
|
361
|
-
sp.fail(e.message);
|
|
362
|
-
printError(e.message);
|
|
357
|
+
sp.fail(e.message.split("\n")[0]);
|
|
358
|
+
printError(e.message.split("\n")[0]);
|
|
363
359
|
}
|
|
364
360
|
break;
|
|
365
361
|
}
|
|
366
362
|
|
|
367
|
-
printInfo("usage: /skill [list | add | remove |
|
|
363
|
+
printInfo("usage: /skill [list | add | remove | find | update | init]");
|
|
364
|
+
printInfo(" /skill add vercel-labs/agent-skills");
|
|
365
|
+
printInfo(" /skill add anthropics/skills --skill frontend-design");
|
|
366
|
+
printInfo(" /skill add owner/repo --global");
|
|
368
367
|
break;
|
|
369
368
|
}
|
|
370
369
|
|
package/package.json
CHANGED
package/src/skills.js
CHANGED
|
@@ -1,218 +1,189 @@
|
|
|
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
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return skills;
|
|
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
|
+
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() };
|
|
71
44
|
}
|
|
72
45
|
|
|
73
46
|
// ─────────────────────────────────────────────────────────────────
|
|
74
|
-
//
|
|
47
|
+
// Install — npx skills add <source> -a gemini -y [--global]
|
|
75
48
|
// ─────────────────────────────────────────────────────────────────
|
|
76
|
-
export function
|
|
77
|
-
|
|
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;
|
|
78
55
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
)
|
|
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 '*'";
|
|
82
60
|
|
|
83
|
-
|
|
61
|
+
const { stdout, stderr } = await npxSkills(args);
|
|
62
|
+
return { output: stdout || stderr };
|
|
84
63
|
}
|
|
85
64
|
|
|
86
65
|
// ─────────────────────────────────────────────────────────────────
|
|
87
|
-
//
|
|
66
|
+
// List installed — npx skills list
|
|
88
67
|
// ─────────────────────────────────────────────────────────────────
|
|
89
|
-
async function
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
headers: { "User-Agent": "gemini-cli/2.0" },
|
|
94
|
-
timeout: 15000,
|
|
95
|
-
});
|
|
96
|
-
return res.data;
|
|
68
|
+
export async function listNpxSkills(global = false) {
|
|
69
|
+
const args = global ? "list -g" : "list";
|
|
70
|
+
const { stdout } = await npxSkills(args);
|
|
71
|
+
return stdout;
|
|
97
72
|
}
|
|
98
73
|
|
|
99
74
|
// ─────────────────────────────────────────────────────────────────
|
|
100
|
-
// Search skills
|
|
75
|
+
// Search — npx skills find <query>
|
|
101
76
|
// ─────────────────────────────────────────────────────────────────
|
|
102
|
-
export async function
|
|
103
|
-
const
|
|
104
|
-
return
|
|
77
|
+
export async function findSkills(query) {
|
|
78
|
+
const { stdout } = await npxSkills(`find ${JSON.stringify(query)}`);
|
|
79
|
+
return stdout;
|
|
105
80
|
}
|
|
106
81
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────
|
|
83
|
+
// Remove — npx skills remove <slug>
|
|
84
|
+
// ─────────────────────────────────────────────────────────────────
|
|
85
|
+
export async function removeSkillNpx(slug) {
|
|
86
|
+
const { stdout, stderr } = await npxSkills(`remove ${slug} -y`);
|
|
87
|
+
return { output: stdout || stderr };
|
|
110
88
|
}
|
|
111
89
|
|
|
112
90
|
// ─────────────────────────────────────────────────────────────────
|
|
113
|
-
//
|
|
114
|
-
// id format: "owner/repo/slug" e.g. "vercel-labs/agent-skills/next-js-development"
|
|
91
|
+
// Update — npx skills update [slug]
|
|
115
92
|
// ─────────────────────────────────────────────────────────────────
|
|
116
|
-
export async function
|
|
117
|
-
const
|
|
118
|
-
|
|
93
|
+
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 };
|
|
119
97
|
}
|
|
120
98
|
|
|
121
99
|
// ─────────────────────────────────────────────────────────────────
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (!skill.files?.length) {
|
|
130
|
-
throw new Error(`Skill "${id}" has no files.`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const targetDir = scope === "global"
|
|
134
|
-
? GLOBAL_AGENTS_DIR
|
|
135
|
-
: path.join(process.cwd(), ".agents");
|
|
136
|
-
|
|
137
|
-
const slug = skill.slug ?? id.split("/").pop();
|
|
138
|
-
const skillDir = path.join(targetDir, slug);
|
|
139
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
140
|
-
|
|
141
|
-
// Write all skill files
|
|
142
|
-
for (const file of skill.files) {
|
|
143
|
-
const filePath = path.join(skillDir, file.path);
|
|
144
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
145
|
-
fs.writeFileSync(filePath, file.contents ?? "", "utf8");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Write metadata
|
|
149
|
-
fs.writeFileSync(path.join(skillDir, ".meta.json"), JSON.stringify({
|
|
150
|
-
id: skill.id,
|
|
151
|
-
name: skill.name ?? slug,
|
|
152
|
-
slug,
|
|
153
|
-
source: skill.source,
|
|
154
|
-
installs: skill.installs,
|
|
155
|
-
installedAt: new Date().toISOString(),
|
|
156
|
-
scope,
|
|
157
|
-
}, null, 2));
|
|
158
|
-
|
|
159
|
-
return { slug, name: skill.name ?? slug, dir: skillDir };
|
|
100
|
+
// Init — npx skills init [name] (creates a SKILL.md template)
|
|
101
|
+
// ─────────────────────────────────────────────────────────────────
|
|
102
|
+
export async function initSkill(name = "") {
|
|
103
|
+
const args = name ? `init "${name}"` : "init";
|
|
104
|
+
const { stdout, stderr } = await npxSkills(args);
|
|
105
|
+
return { output: stdout || stderr };
|
|
160
106
|
}
|
|
161
107
|
|
|
162
108
|
// ─────────────────────────────────────────────────────────────────
|
|
163
|
-
//
|
|
109
|
+
// Load skills for agent context injection
|
|
110
|
+
// Scans all skill directories and reads SKILL.md files
|
|
164
111
|
// ─────────────────────────────────────────────────────────────────
|
|
165
|
-
export function
|
|
166
|
-
const
|
|
112
|
+
export function loadSkills() {
|
|
113
|
+
const skills = [];
|
|
114
|
+
const seen = new Set();
|
|
167
115
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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;
|
|
173
122
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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 });
|
|
179
138
|
}
|
|
180
|
-
}
|
|
139
|
+
};
|
|
181
140
|
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
|
|
146
|
+
return skills;
|
|
184
147
|
}
|
|
185
148
|
|
|
186
149
|
// ─────────────────────────────────────────────────────────────────
|
|
187
|
-
//
|
|
150
|
+
// Build skills section injected into agent system prompt
|
|
151
|
+
// ─────────────────────────────────────────────────────────────────
|
|
152
|
+
export function buildSkillsPrompt(skills) {
|
|
153
|
+
if (!skills.length) return null;
|
|
154
|
+
|
|
155
|
+
const sections = skills.map(s =>
|
|
156
|
+
`### Skill: ${s.name}\n${s.content.trim()}`
|
|
157
|
+
).join("\n\n---\n\n");
|
|
158
|
+
|
|
159
|
+
return `## INSTALLED SKILLS\n\nApply the following skills when relevant to the current task:\n\n${sections}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─────────────────────────────────────────────────────────────────
|
|
163
|
+
// Quick disk check — list installed skills without running npx
|
|
188
164
|
// ─────────────────────────────────────────────────────────────────
|
|
189
165
|
export function listInstalledSkills() {
|
|
190
166
|
const result = [];
|
|
191
167
|
|
|
192
|
-
const
|
|
168
|
+
const scanDir = (dir, isGlobal) => {
|
|
193
169
|
if (!fs.existsSync(dir)) return;
|
|
194
170
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
195
171
|
if (!entry.isDirectory()) continue;
|
|
196
172
|
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
|
-
|
|
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);
|
|
204
176
|
result.push({
|
|
205
177
|
slug: entry.name,
|
|
206
|
-
name:
|
|
207
|
-
source: meta.source ?? "local",
|
|
178
|
+
name: nameMatch?.[1]?.trim() ?? entry.name,
|
|
208
179
|
global: isGlobal,
|
|
209
180
|
dir: skillDir,
|
|
210
|
-
installedAt: meta.installedAt ?? null,
|
|
211
181
|
});
|
|
212
182
|
}
|
|
213
183
|
};
|
|
214
184
|
|
|
215
|
-
|
|
216
|
-
|
|
185
|
+
scanDir(PROJECT_SKILLS_DIR(), false);
|
|
186
|
+
scanDir(CUSTOM_SKILLS_DIR(), false);
|
|
187
|
+
scanDir(GLOBAL_SKILLS_DIR, true);
|
|
217
188
|
return result;
|
|
218
189
|
}
|