@ikyyofc/gemini-cli 3.0.1 → 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 +141 -3
- package/package.json +1 -1
- package/src/agent.js +6 -2
- package/src/renderer.js +13 -3
- package/src/skills.js +189 -0
package/index.js
CHANGED
|
@@ -24,11 +24,17 @@ import {
|
|
|
24
24
|
} from "./src/input.js";
|
|
25
25
|
import { setupGlobalProxy, proxyStatus, setProxyEnabled } from "./src/utils/proxy.js";
|
|
26
26
|
import { Spinner } from "./src/utils/spinner.js";
|
|
27
|
+
import {
|
|
28
|
+
listInstalledSkills, installSkill, removeSkillNpx,
|
|
29
|
+
findSkills, listNpxSkills, updateSkill, initSkill,
|
|
30
|
+
ensureSkillsDirs, loadSkills,
|
|
31
|
+
} from "./src/skills.js";
|
|
27
32
|
|
|
28
33
|
// ─────────────────────────────────────────────────────────────────
|
|
29
34
|
// Bootstrap
|
|
30
35
|
// ─────────────────────────────────────────────────────────────────
|
|
31
36
|
ensureGlobalDir();
|
|
37
|
+
ensureSkillsDirs();
|
|
32
38
|
setupGlobalProxy(); // aktifkan rotasi proxy sebelum request apapun
|
|
33
39
|
|
|
34
40
|
let extensions = loadExtensions();
|
|
@@ -231,6 +237,136 @@ async function handleCommand(input) {
|
|
|
231
237
|
printInfo("conversation cleared");
|
|
232
238
|
break;
|
|
233
239
|
|
|
240
|
+
case "/skill": case "/skills": {
|
|
241
|
+
const sub = tokens[1]?.toLowerCase();
|
|
242
|
+
const rest = tokens.slice(2).join(" ").trim();
|
|
243
|
+
|
|
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>");
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
console.log("");
|
|
254
|
+
disk.forEach(s => {
|
|
255
|
+
const scope = s.global ? chalk.dim("global") : chalk.hex("#4EC9B0")("project");
|
|
256
|
+
console.log(
|
|
257
|
+
" " + chalk.hex("#FFD080").bold(s.name.padEnd(30)) +
|
|
258
|
+
chalk.dim(s.slug.padEnd(26)) + scope
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
console.log(chalk.dim(`\n ${disk.length} skill(s) active in agent context\n`));
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── /skill add <source> [--global] [--skill <name>] ──
|
|
266
|
+
if (sub === "add" || sub === "install") {
|
|
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
|
+
|
|
277
|
+
const sp = new Spinner();
|
|
278
|
+
sp.start(`npx skills add ${source}…`, "#FFD080");
|
|
279
|
+
try {
|
|
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");
|
|
285
|
+
} catch (e) {
|
|
286
|
+
sp.fail(e.message.split("\n")[0]);
|
|
287
|
+
printError(e.message.split("\n")[0]);
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── /skill remove <slug> ──────────────────────────────
|
|
293
|
+
if (sub === "remove" || sub === "rm") {
|
|
294
|
+
if (!rest) { printError("usage: /skill remove <slug>"); break; }
|
|
295
|
+
const sp = new Spinner();
|
|
296
|
+
sp.start(`removing ${rest}…`, "#FF9060");
|
|
297
|
+
try {
|
|
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
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── /skill find / search <query> ─────────────────────
|
|
310
|
+
if (sub === "find" || sub === "search") {
|
|
311
|
+
if (!rest) { printError("usage: /skill find <query>"); break; }
|
|
312
|
+
const sp = new Spinner();
|
|
313
|
+
sp.start(`searching "${rest}"…`, "#4A9EFF");
|
|
314
|
+
try {
|
|
315
|
+
const output = await findSkills(rest);
|
|
316
|
+
sp.stop();
|
|
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
|
+
}
|
|
324
|
+
} catch (e) {
|
|
325
|
+
sp.fail(e.message.split("\n")[0]);
|
|
326
|
+
printError(e.message.split("\n")[0]);
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ── /skill update [slug] ──────────────────────────────
|
|
332
|
+
if (sub === "update") {
|
|
333
|
+
const sp = new Spinner();
|
|
334
|
+
sp.start(rest ? `updating ${rest}…` : "updating all skills…", "#4A9EFF");
|
|
335
|
+
try {
|
|
336
|
+
const { output } = await updateSkill(rest);
|
|
337
|
+
sp.stop();
|
|
338
|
+
if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
|
|
339
|
+
printSuccess("updated");
|
|
340
|
+
} catch (e) {
|
|
341
|
+
sp.fail(e.message.split("\n")[0]);
|
|
342
|
+
printError(e.message.split("\n")[0]);
|
|
343
|
+
}
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── /skill init [name] ────────────────────────────────
|
|
348
|
+
if (sub === "init") {
|
|
349
|
+
const sp = new Spinner();
|
|
350
|
+
sp.start("creating SKILL.md template…", "#4A9EFF");
|
|
351
|
+
try {
|
|
352
|
+
const { output } = await initSkill(rest);
|
|
353
|
+
sp.stop();
|
|
354
|
+
if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
|
|
355
|
+
printSuccess("SKILL.md created");
|
|
356
|
+
} catch (e) {
|
|
357
|
+
sp.fail(e.message.split("\n")[0]);
|
|
358
|
+
printError(e.message.split("\n")[0]);
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
|
|
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");
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
|
|
234
370
|
case "/file":
|
|
235
371
|
if (!arg) { printError("usage: /file <path>"); break; }
|
|
236
372
|
attachFile(arg); break;
|
|
@@ -282,12 +418,14 @@ async function handleCommand(input) {
|
|
|
282
418
|
break;
|
|
283
419
|
|
|
284
420
|
case "/model": {
|
|
285
|
-
const px
|
|
421
|
+
const px = proxyStatus();
|
|
422
|
+
const sk = loadSkills();
|
|
286
423
|
printInfo(`model gemini-pro-latest`);
|
|
287
424
|
printInfo(`tools ${agentMode ? "on (native function calling)" : "off"}`);
|
|
288
425
|
printInfo(`yolo ${autoApprove ? "on" : "off"}`);
|
|
289
426
|
printInfo(`memory ${memoryLoaded.length} file(s) · extensions: ${extensions.length}`);
|
|
290
|
-
printInfo(`
|
|
427
|
+
printInfo(`skills ${sk.length} active · .agents/ + ~/.gemini/agents/`);
|
|
428
|
+
printInfo(`proxy ${px.available}/${px.total} available · ${px.enabled ? "on" : "off"}`);
|
|
291
429
|
printInfo(`config ${GLOBAL_DIR}`);
|
|
292
430
|
break;
|
|
293
431
|
}
|
|
@@ -324,7 +462,7 @@ async function handleCommand(input) {
|
|
|
324
462
|
// ─────────────────────────────────────────────────────────────────
|
|
325
463
|
async function main() {
|
|
326
464
|
process.stdout.write("\x1Bc");
|
|
327
|
-
console.log(renderWelcome(memoryLoaded.length, extensions.length));
|
|
465
|
+
console.log(renderWelcome(memoryLoaded.length, extensions.length, loadSkills().length));
|
|
328
466
|
|
|
329
467
|
const argv = process.argv.slice(2);
|
|
330
468
|
const positional = [];
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -3,6 +3,7 @@ import chalk from "chalk";
|
|
|
3
3
|
import { callGemini } from "./gemini.js";
|
|
4
4
|
import { GEMINI_TOOLS, FUNCTION_DECLARATIONS, executeTool } from "./tools.js";
|
|
5
5
|
import { Spinner } from "./utils/spinner.js";
|
|
6
|
+
import { loadSkills, buildSkillsPrompt } from "./skills.js";
|
|
6
7
|
import {
|
|
7
8
|
printAssistant, printError, printWarning,
|
|
8
9
|
printToolCall, printToolResult,
|
|
@@ -15,7 +16,10 @@ const TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
|
15
16
|
// System prompt
|
|
16
17
|
// ─────────────────────────────────────────────────────────────────
|
|
17
18
|
function buildSystemPrompt(extra = "") {
|
|
18
|
-
const toolList
|
|
19
|
+
const toolList = FUNCTION_DECLARATIONS.map(t => `- ${t.name}: ${t.description}`).join("\n");
|
|
20
|
+
const skills = loadSkills();
|
|
21
|
+
const skillsBlock = buildSkillsPrompt(skills);
|
|
22
|
+
|
|
19
23
|
return `You are an autonomous AI coding agent running in the user's terminal. You have full access to their filesystem and shell through tools.
|
|
20
24
|
|
|
21
25
|
## CORE RULE — NEVER ASK, ALWAYS ACT
|
|
@@ -49,7 +53,7 @@ ${toolList}
|
|
|
49
53
|
- Current working directory: ${process.cwd()}
|
|
50
54
|
- Platform: ${process.platform}
|
|
51
55
|
|
|
52
|
-
${extra ? `## EXTRA INSTRUCTIONS\n${extra}` : ""}`.trim();
|
|
56
|
+
${skillsBlock ? skillsBlock + "\n" : ""}${extra ? `## EXTRA INSTRUCTIONS\n${extra}` : ""}`.trim();
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
// ─────────────────────────────────────────────────────────────────
|
package/src/renderer.js
CHANGED
|
@@ -324,11 +324,12 @@ export function printWarning(msg) { process.stdout.write(chalk.hex(C.yellow)("
|
|
|
324
324
|
// ─────────────────────────────────────────────────────────────────
|
|
325
325
|
// Welcome & Help
|
|
326
326
|
// ─────────────────────────────────────────────────────────────────
|
|
327
|
-
export function renderWelcome(memCount = 0, extCount = 0) {
|
|
327
|
+
export function renderWelcome(memCount = 0, extCount = 0, skillCount = 0) {
|
|
328
328
|
const W = bw();
|
|
329
329
|
const stats = [
|
|
330
|
-
memCount
|
|
331
|
-
extCount
|
|
330
|
+
memCount ? `${memCount} context file${memCount > 1 ? "s" : ""}` : null,
|
|
331
|
+
extCount ? `${extCount} extension${extCount > 1 ? "s" : ""}` : null,
|
|
332
|
+
skillCount ? `${skillCount} skill${skillCount > 1 ? "s" : ""} active` : null,
|
|
332
333
|
].filter(Boolean).join(" · ");
|
|
333
334
|
return [
|
|
334
335
|
"",
|
|
@@ -366,6 +367,15 @@ export function renderHelp(customCommands = {}) {
|
|
|
366
367
|
row("/new /clear", "Reset conversation"),
|
|
367
368
|
row("/model", "Model & config info"),
|
|
368
369
|
row("/proxy [on|off]", "Proxy rotation status/toggle"),
|
|
370
|
+
" " + sep,
|
|
371
|
+
chalk.hex(C.yellow).bold(" Skills · skills.sh"),
|
|
372
|
+
" " + sep,
|
|
373
|
+
row("/skill list", "List installed skills"),
|
|
374
|
+
row("/skill add <id>", "Install from skills.sh (--global for global)"),
|
|
375
|
+
row("/skill remove <n>", "Uninstall a skill"),
|
|
376
|
+
row("/skill search <q>", "Search skills.sh registry"),
|
|
377
|
+
row("/skill browse", "Browse top skills (trending/hot)"),
|
|
378
|
+
row("/skill info <id>", "Show skill details"),
|
|
369
379
|
row("/exit /quit", "Exit"), sep,
|
|
370
380
|
chalk.hex(C.dim)(" Ctrl+C interrupt · Ctrl+D exit"), "",
|
|
371
381
|
];
|
package/src/skills.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
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)
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import os from "os";
|
|
12
|
+
import { promisify } from "util";
|
|
13
|
+
import { exec } from "child_process";
|
|
14
|
+
|
|
15
|
+
const execAsync = promisify(exec);
|
|
16
|
+
|
|
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");
|
|
21
|
+
|
|
22
|
+
export function ensureSkillsDirs() {
|
|
23
|
+
fs.mkdirSync(GLOBAL_SKILLS_DIR, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────
|
|
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() };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────
|
|
47
|
+
// Install — npx skills add <source> -a gemini -y [--global]
|
|
48
|
+
// ─────────────────────────────────────────────────────────────────
|
|
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 };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────
|
|
66
|
+
// List installed — npx skills list
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────
|
|
68
|
+
export async function listNpxSkills(global = false) {
|
|
69
|
+
const args = global ? "list -g" : "list";
|
|
70
|
+
const { stdout } = await npxSkills(args);
|
|
71
|
+
return stdout;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────
|
|
75
|
+
// Search — npx skills find <query>
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────
|
|
77
|
+
export async function findSkills(query) {
|
|
78
|
+
const { stdout } = await npxSkills(`find ${JSON.stringify(query)}`);
|
|
79
|
+
return stdout;
|
|
80
|
+
}
|
|
81
|
+
|
|
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 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────
|
|
91
|
+
// Update — npx skills update [slug]
|
|
92
|
+
// ─────────────────────────────────────────────────────────────────
|
|
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 };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─────────────────────────────────────────────────────────────────
|
|
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 };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─────────────────────────────────────────────────────────────────
|
|
109
|
+
// Load skills for agent context injection
|
|
110
|
+
// Scans all skill directories and reads SKILL.md files
|
|
111
|
+
// ─────────────────────────────────────────────────────────────────
|
|
112
|
+
export function loadSkills() {
|
|
113
|
+
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
|
+
|
|
146
|
+
return skills;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─────────────────────────────────────────────────────────────────
|
|
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
|
|
164
|
+
// ─────────────────────────────────────────────────────────────────
|
|
165
|
+
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;
|
|
189
|
+
}
|