@ikyyofc/gemini-cli 3.0.1 → 3.0.2
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 +142 -3
- package/package.json +1 -1
- package/src/agent.js +6 -2
- package/src/renderer.js +13 -3
- package/src/skills.js +218 -0
package/index.js
CHANGED
|
@@ -24,11 +24,16 @@ 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, removeSkill,
|
|
29
|
+
searchSkills, browseSkills, ensureSkillsDirs, loadSkills,
|
|
30
|
+
} from "./src/skills.js";
|
|
27
31
|
|
|
28
32
|
// ─────────────────────────────────────────────────────────────────
|
|
29
33
|
// Bootstrap
|
|
30
34
|
// ─────────────────────────────────────────────────────────────────
|
|
31
35
|
ensureGlobalDir();
|
|
36
|
+
ensureSkillsDirs();
|
|
32
37
|
setupGlobalProxy(); // aktifkan rotasi proxy sebelum request apapun
|
|
33
38
|
|
|
34
39
|
let extensions = loadExtensions();
|
|
@@ -231,6 +236,138 @@ async function handleCommand(input) {
|
|
|
231
236
|
printInfo("conversation cleared");
|
|
232
237
|
break;
|
|
233
238
|
|
|
239
|
+
case "/skill": case "/skills": {
|
|
240
|
+
const sub = tokens[1]?.toLowerCase();
|
|
241
|
+
const rest = tokens.slice(2).join(" ").trim();
|
|
242
|
+
|
|
243
|
+
// /skill list
|
|
244
|
+
if (!sub || sub === "list") {
|
|
245
|
+
const list = listInstalledSkills();
|
|
246
|
+
if (!list.length) {
|
|
247
|
+
printInfo("no skills installed · use /skill add <id> to install");
|
|
248
|
+
printInfo("browse skills at https://skills.sh");
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
console.log("");
|
|
252
|
+
list.forEach(s => {
|
|
253
|
+
const scope = s.global ? chalk.dim("global") : chalk.hex("#4EC9B0")("project");
|
|
254
|
+
console.log(
|
|
255
|
+
" " + chalk.hex("#FFD080").bold(s.slug.padEnd(28)) +
|
|
256
|
+
chalk.dim(s.source.padEnd(30)) + scope
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
console.log(chalk.dim(`\n ${list.length} skill(s) loaded into agent context\n`));
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// /skill add <id> [--global]
|
|
264
|
+
if (sub === "add" || sub === "install") {
|
|
265
|
+
const isGlobal = rest.includes("--global");
|
|
266
|
+
const id = rest.replace("--global", "").trim();
|
|
267
|
+
if (!id) { printError("usage: /skill add <owner/repo/slug> [--global]"); break; }
|
|
268
|
+
const sp = new Spinner();
|
|
269
|
+
sp.start(`installing ${id}…`, "#FFD080");
|
|
270
|
+
try {
|
|
271
|
+
const result = await installSkill(id, isGlobal ? "global" : "project");
|
|
272
|
+
sp.succeed(`installed: ${result.name} → ${result.dir}`);
|
|
273
|
+
printInfo("skill will be active on next message");
|
|
274
|
+
} catch (e) {
|
|
275
|
+
sp.fail(e.message);
|
|
276
|
+
printError(e.message);
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// /skill remove <slug>
|
|
282
|
+
if (sub === "remove" || sub === "rm") {
|
|
283
|
+
const slug = rest;
|
|
284
|
+
if (!slug) { printError("usage: /skill remove <slug>"); break; }
|
|
285
|
+
try {
|
|
286
|
+
const removed = removeSkill(slug);
|
|
287
|
+
removed.forEach(r => printSuccess(`removed: ${r}`));
|
|
288
|
+
} catch (e) { printError(e.message); }
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// /skill search <query>
|
|
293
|
+
if (sub === "search") {
|
|
294
|
+
if (!rest) { printError("usage: /skill search <query>"); break; }
|
|
295
|
+
const sp = new Spinner();
|
|
296
|
+
sp.start(`searching "${rest}"…`, "#4A9EFF");
|
|
297
|
+
try {
|
|
298
|
+
const results = await searchSkills(rest, 8);
|
|
299
|
+
sp.stop();
|
|
300
|
+
if (!results.length) { printInfo("no results found"); break; }
|
|
301
|
+
console.log("");
|
|
302
|
+
results.forEach(r => {
|
|
303
|
+
console.log(
|
|
304
|
+
" " + chalk.hex("#4A9EFF").bold(r.id.padEnd(48)) +
|
|
305
|
+
chalk.dim(String(r.installs).padStart(8) + " installs")
|
|
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"));
|
|
311
|
+
} catch (e) {
|
|
312
|
+
sp.fail(e.message);
|
|
313
|
+
printError(e.message);
|
|
314
|
+
}
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// /skill browse [trending|hot]
|
|
319
|
+
if (sub === "browse") {
|
|
320
|
+
const view = rest || "all-time";
|
|
321
|
+
const sp = new Spinner();
|
|
322
|
+
sp.start(`fetching ${view}…`, "#4A9EFF");
|
|
323
|
+
try {
|
|
324
|
+
const results = await browseSkills(view, 15);
|
|
325
|
+
sp.stop();
|
|
326
|
+
console.log("");
|
|
327
|
+
results.forEach((r, i) => {
|
|
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"));
|
|
335
|
+
} catch (e) {
|
|
336
|
+
sp.fail(e.message);
|
|
337
|
+
printError(e.message);
|
|
338
|
+
}
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// /skill info <id>
|
|
343
|
+
if (sub === "info") {
|
|
344
|
+
if (!rest) { printError("usage: /skill info <id>"); break; }
|
|
345
|
+
const sp = new Spinner();
|
|
346
|
+
sp.start(`fetching ${rest}…`, "#4A9EFF");
|
|
347
|
+
try {
|
|
348
|
+
const { default: ax } = await import("axios");
|
|
349
|
+
const { data } = await ax.get(`https://skills.sh/api/v1/skills/${rest}`);
|
|
350
|
+
sp.stop();
|
|
351
|
+
console.log("");
|
|
352
|
+
printInfo(`id ${data.id}`);
|
|
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`));
|
|
360
|
+
} catch (e) {
|
|
361
|
+
sp.fail(e.message);
|
|
362
|
+
printError(e.message);
|
|
363
|
+
}
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
printInfo("usage: /skill [list | add | remove | search | browse | info]");
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
|
|
234
371
|
case "/file":
|
|
235
372
|
if (!arg) { printError("usage: /file <path>"); break; }
|
|
236
373
|
attachFile(arg); break;
|
|
@@ -282,12 +419,14 @@ async function handleCommand(input) {
|
|
|
282
419
|
break;
|
|
283
420
|
|
|
284
421
|
case "/model": {
|
|
285
|
-
const px
|
|
422
|
+
const px = proxyStatus();
|
|
423
|
+
const sk = loadSkills();
|
|
286
424
|
printInfo(`model gemini-pro-latest`);
|
|
287
425
|
printInfo(`tools ${agentMode ? "on (native function calling)" : "off"}`);
|
|
288
426
|
printInfo(`yolo ${autoApprove ? "on" : "off"}`);
|
|
289
427
|
printInfo(`memory ${memoryLoaded.length} file(s) · extensions: ${extensions.length}`);
|
|
290
|
-
printInfo(`
|
|
428
|
+
printInfo(`skills ${sk.length} active · .agents/ + ~/.gemini/agents/`);
|
|
429
|
+
printInfo(`proxy ${px.available}/${px.total} available · ${px.enabled ? "on" : "off"}`);
|
|
291
430
|
printInfo(`config ${GLOBAL_DIR}`);
|
|
292
431
|
break;
|
|
293
432
|
}
|
|
@@ -324,7 +463,7 @@ async function handleCommand(input) {
|
|
|
324
463
|
// ─────────────────────────────────────────────────────────────────
|
|
325
464
|
async function main() {
|
|
326
465
|
process.stdout.write("\x1Bc");
|
|
327
|
-
console.log(renderWelcome(memoryLoaded.length, extensions.length));
|
|
466
|
+
console.log(renderWelcome(memoryLoaded.length, extensions.length, loadSkills().length));
|
|
328
467
|
|
|
329
468
|
const argv = process.argv.slice(2);
|
|
330
469
|
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,218 @@
|
|
|
1
|
+
// src/skills.js — Agent Skills Manager
|
|
2
|
+
// Integrates with skills.sh registry
|
|
3
|
+
// Skills are stored in .agents/ (project) or ~/.gemini/agents/ (global)
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import axios from "axios";
|
|
8
|
+
|
|
9
|
+
const API = "https://skills.sh/api/v1";
|
|
10
|
+
const GLOBAL_AGENTS_DIR = path.join(os.homedir(), ".gemini", "agents");
|
|
11
|
+
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────
|
|
13
|
+
// Resolve skills directories
|
|
14
|
+
// Priority: .agents/ (project) → ~/.gemini/agents/ (global)
|
|
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
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ensureSkillsDirs() {
|
|
25
|
+
fs.mkdirSync(GLOBAL_AGENTS_DIR, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────
|
|
29
|
+
// Load all installed skills — returns array of { name, path, content }
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────
|
|
31
|
+
export function loadSkills() {
|
|
32
|
+
const skills = [];
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
|
|
35
|
+
for (const dir of getSkillsDirs()) {
|
|
36
|
+
if (!fs.existsSync(dir)) continue;
|
|
37
|
+
|
|
38
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
39
|
+
if (!entry.isDirectory()) continue;
|
|
40
|
+
const skillDir = path.join(dir, entry.name);
|
|
41
|
+
const skillMd = path.join(skillDir, "SKILL.md");
|
|
42
|
+
const metaFile = path.join(skillDir, ".meta.json");
|
|
43
|
+
|
|
44
|
+
if (!fs.existsSync(skillMd)) continue;
|
|
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
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return skills;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─────────────────────────────────────────────────────────────────
|
|
74
|
+
// Build skills section for system prompt
|
|
75
|
+
// ─────────────────────────────────────────────────────────────────
|
|
76
|
+
export function buildSkillsPrompt(skills) {
|
|
77
|
+
if (!skills.length) return null;
|
|
78
|
+
|
|
79
|
+
const sections = skills.map(s =>
|
|
80
|
+
`### Skill: ${s.name}\n${s.content.trim()}`
|
|
81
|
+
).join("\n\n---\n\n");
|
|
82
|
+
|
|
83
|
+
return `## INSTALLED SKILLS\n\nThe following skills provide additional knowledge and capabilities. Apply them when relevant:\n\n${sections}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─────────────────────────────────────────────────────────────────
|
|
87
|
+
// API helpers
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────
|
|
89
|
+
async function apiFetch(path, params = {}) {
|
|
90
|
+
const url = `${API}${path}`;
|
|
91
|
+
const res = await axios.get(url, {
|
|
92
|
+
params,
|
|
93
|
+
headers: { "User-Agent": "gemini-cli/2.0" },
|
|
94
|
+
timeout: 15000,
|
|
95
|
+
});
|
|
96
|
+
return res.data;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─────────────────────────────────────────────────────────────────
|
|
100
|
+
// Search skills
|
|
101
|
+
// ─────────────────────────────────────────────────────────────────
|
|
102
|
+
export async function searchSkills(query, limit = 10) {
|
|
103
|
+
const data = await apiFetch("/skills/search", { q: query, limit });
|
|
104
|
+
return data.data ?? [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function browseSkills(view = "all-time", perPage = 20) {
|
|
108
|
+
const data = await apiFetch("/skills", { view, per_page: perPage });
|
|
109
|
+
return data.data ?? [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─────────────────────────────────────────────────────────────────
|
|
113
|
+
// Fetch skill files from API
|
|
114
|
+
// id format: "owner/repo/slug" e.g. "vercel-labs/agent-skills/next-js-development"
|
|
115
|
+
// ─────────────────────────────────────────────────────────────────
|
|
116
|
+
export async function fetchSkill(id) {
|
|
117
|
+
const data = await apiFetch(`/skills/${id}`);
|
|
118
|
+
return data; // { id, slug, name?, files: [{ path, contents }] }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─────────────────────────────────────────────────────────────────
|
|
122
|
+
// Install a skill
|
|
123
|
+
// source: "owner/repo/slug" or just "owner/repo" (installs all)
|
|
124
|
+
// scope: "project" | "global"
|
|
125
|
+
// ─────────────────────────────────────────────────────────────────
|
|
126
|
+
export async function installSkill(id, scope = "project") {
|
|
127
|
+
const skill = await fetchSkill(id);
|
|
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 };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─────────────────────────────────────────────────────────────────
|
|
163
|
+
// Remove a skill
|
|
164
|
+
// ─────────────────────────────────────────────────────────────────
|
|
165
|
+
export function removeSkill(slug, scope = "both") {
|
|
166
|
+
const removed = [];
|
|
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];
|
|
173
|
+
|
|
174
|
+
for (const dir of dirs) {
|
|
175
|
+
const skillDir = path.join(dir, slug);
|
|
176
|
+
if (fs.existsSync(skillDir)) {
|
|
177
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
178
|
+
removed.push(skillDir);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!removed.length) throw new Error(`Skill "${slug}" not found.`);
|
|
183
|
+
return removed;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─────────────────────────────────────────────────────────────────
|
|
187
|
+
// List installed skills with status
|
|
188
|
+
// ─────────────────────────────────────────────────────────────────
|
|
189
|
+
export function listInstalledSkills() {
|
|
190
|
+
const result = [];
|
|
191
|
+
|
|
192
|
+
const checkDir = (dir, isGlobal) => {
|
|
193
|
+
if (!fs.existsSync(dir)) return;
|
|
194
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
195
|
+
if (!entry.isDirectory()) continue;
|
|
196
|
+
const skillDir = path.join(dir, entry.name);
|
|
197
|
+
const metaFile = path.join(skillDir, ".meta.json");
|
|
198
|
+
const hasSkillMd = fs.existsSync(path.join(skillDir, "SKILL.md"));
|
|
199
|
+
if (!hasSkillMd) continue;
|
|
200
|
+
|
|
201
|
+
let meta = {};
|
|
202
|
+
try { meta = JSON.parse(fs.readFileSync(metaFile, "utf8")); } catch {}
|
|
203
|
+
|
|
204
|
+
result.push({
|
|
205
|
+
slug: entry.name,
|
|
206
|
+
name: meta.name ?? entry.name,
|
|
207
|
+
source: meta.source ?? "local",
|
|
208
|
+
global: isGlobal,
|
|
209
|
+
dir: skillDir,
|
|
210
|
+
installedAt: meta.installedAt ?? null,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
checkDir(path.join(process.cwd(), ".agents"), false);
|
|
216
|
+
checkDir(GLOBAL_AGENTS_DIR, true);
|
|
217
|
+
return result;
|
|
218
|
+
}
|