@ikyyofc/gemini-cli 3.0.0 → 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 +218 -91
- 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
|
@@ -13,16 +13,18 @@ const tw = () => Math.min(process.stdout.columns || 72, 84);
|
|
|
13
13
|
const bw = () => Math.min(tw() - 4, 68);
|
|
14
14
|
const vlen = s => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// Word-wrap a PLAIN (no ANSI) string to maxW, returns array of lines
|
|
17
|
+
function wrapPlain(text, maxW) {
|
|
18
|
+
if (!text || text.length <= maxW) return [text || ""];
|
|
18
19
|
const words = text.split(" ");
|
|
19
20
|
const lines = [];
|
|
20
21
|
let cur = "";
|
|
21
22
|
for (const w of words) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
23
|
+
const candidate = cur ? cur + " " + w : w;
|
|
24
|
+
if (candidate.length <= maxW) { cur = candidate; continue; }
|
|
24
25
|
if (cur) lines.push(cur);
|
|
25
|
-
if (
|
|
26
|
+
if (w.length > maxW) {
|
|
27
|
+
// hard-wrap single very long word (e.g. bare URL)
|
|
26
28
|
for (let i = 0; i < w.length; i += maxW) lines.push(w.slice(i, i + maxW));
|
|
27
29
|
cur = "";
|
|
28
30
|
} else { cur = w; }
|
|
@@ -31,37 +33,34 @@ function wrapText(text, maxW) {
|
|
|
31
33
|
return lines.length ? lines : [""];
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// garbage numbers appearing in rendered code).
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────
|
|
37
|
+
// Syntax highlighting — applyToRaw prevents placeholder corruption
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────
|
|
38
39
|
const KW = {
|
|
39
|
-
js
|
|
40
|
-
ts
|
|
41
|
-
py
|
|
42
|
-
go
|
|
43
|
-
sh
|
|
44
|
-
rs
|
|
40
|
+
js:/\b(const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|new|this|class|extends|import|export|default|async|await|try|catch|finally|throw|typeof|instanceof|of|in|null|undefined|true|false|void|delete|yield|from|static|super)\b/g,
|
|
41
|
+
ts:/\b(const|let|var|function|return|if|else|for|while|switch|case|class|extends|import|export|default|async|await|try|catch|type|interface|enum|implements|declare|readonly|abstract|as|keyof|never|any|string|number|boolean|null|undefined|true|false)\b/g,
|
|
42
|
+
py:/\b(def|class|return|if|elif|else|for|while|import|from|as|with|try|except|finally|raise|pass|break|continue|and|or|not|in|is|None|True|False|lambda|yield|global|async|await)\b/g,
|
|
43
|
+
go:/\b(func|return|if|else|for|range|switch|var|const|type|struct|interface|import|package|defer|go|chan|map|make|new|nil|true|false)\b/g,
|
|
44
|
+
sh:/\b(if|then|else|elif|fi|for|while|do|done|case|esac|function|return|exit|export|echo|local|source|cd|mkdir|rm|cp|mv|sudo|npm|pip|git)\b/g,
|
|
45
|
+
rs:/\b(fn|let|mut|return|if|else|for|match|use|mod|pub|struct|enum|impl|trait|type|const|async|await|true|false|None|Some|Ok|Err)\b/g,
|
|
45
46
|
};
|
|
46
47
|
const LANGMAP = {
|
|
47
|
-
javascript:"js",
|
|
48
|
-
python:"py",
|
|
49
|
-
rust:"rs",
|
|
48
|
+
javascript:"js",js:"js",typescript:"ts",ts:"ts",
|
|
49
|
+
python:"py",py:"py",go:"go",golang:"go",
|
|
50
|
+
rust:"rs",rs:"rs",bash:"sh",sh:"sh",shell:"sh",zsh:"sh",fish:"sh",
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
function applyToRaw(str, re, fn) {
|
|
53
|
-
return str.split(/(\x00\d+\x00)/).map((p, i) =>
|
|
54
|
-
i % 2 === 0 ? p.replace(re, fn) : p
|
|
55
|
-
).join("");
|
|
54
|
+
return str.split(/(\x00\d+\x00)/).map((p, i) => i % 2 === 0 ? p.replace(re, fn) : p).join("");
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
function highlight(code, lang = "") {
|
|
59
58
|
const l = LANGMAP[lang.toLowerCase()] || "";
|
|
60
59
|
if (!l) return code;
|
|
61
60
|
const saved = [];
|
|
62
|
-
const save
|
|
61
|
+
const save = s => { const id = `\x00${saved.length}\x00`; saved.push(s); return id; };
|
|
63
62
|
let r = code;
|
|
64
|
-
r = applyToRaw(r, /(\/\/.*$|#.*$|\/\*[\s\S]*?\*\/)/gm,
|
|
63
|
+
r = applyToRaw(r, /(\/\/.*$|#.*$|\/\*[\s\S]*?\*\/)/gm, m => save(chalk.hex(C.comment).italic(m)));
|
|
65
64
|
r = applyToRaw(r, /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, m => save(chalk.hex(C.str)(m)));
|
|
66
65
|
if (KW[l]) { KW[l].lastIndex = 0; r = applyToRaw(r, KW[l], m => save(chalk.hex(C.kw).bold(m))); }
|
|
67
66
|
r = applyToRaw(r, /\b(\d+\.?\d*)\b/g, m => save(chalk.hex(C.num)(m)));
|
|
@@ -69,53 +68,162 @@ function highlight(code, lang = "") {
|
|
|
69
68
|
return r.replace(/\x00(\d+)\x00/g, (_, i) => saved[+i] ?? "");
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
//
|
|
71
|
+
// ─────────────────────────────────────────────────────────────────
|
|
72
|
+
// Markdown → array of display lines (with wrapping baked in)
|
|
73
|
+
// Returns: Array<{ text: string, raw: boolean }>
|
|
74
|
+
// raw=true → ANSI already applied (code blocks), print as-is
|
|
75
|
+
// raw=false → plain text, will receive border + chalk in printBox
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────
|
|
73
77
|
export function renderMarkdown(text, contentW) {
|
|
74
78
|
const cw = contentW ?? bw() - 2;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
79
|
+
const out = []; // { text, raw }
|
|
80
|
+
|
|
81
|
+
const pushPlain = (s) => out.push({ text: s, raw: false });
|
|
82
|
+
const pushStyled = (s) => out.push({ text: s, raw: true });
|
|
83
|
+
const pushBlank = () => out.push({ text: "", raw: false });
|
|
84
|
+
|
|
85
|
+
// Normalise line endings
|
|
86
|
+
const src = text.replace(/\r\n/g, "\n").trimEnd();
|
|
87
|
+
|
|
88
|
+
// Split into tokens: code blocks vs everything else
|
|
89
|
+
const segments = src.split(/(```[\s\S]*?```)/g);
|
|
90
|
+
|
|
91
|
+
for (const seg of segments) {
|
|
92
|
+
// ── Fenced code block ──────────────────────────────────────
|
|
93
|
+
if (seg.startsWith("```")) {
|
|
94
|
+
const m = seg.match(/^```(\w*)\n?([\s\S]*?)```$/);
|
|
95
|
+
const lang = m?.[1] ?? "";
|
|
96
|
+
const code = m?.[2]?.trimEnd() ?? seg.slice(3);
|
|
97
|
+
const lines = code.split("\n");
|
|
98
|
+
const hl = highlight(code, lang);
|
|
99
|
+
const hlLines = hl.split("\n");
|
|
100
|
+
const gw = String(lines.length).length;
|
|
101
|
+
const bw2 = Math.min(cw + 2, tw() - 6);
|
|
102
|
+
const lbl = lang ? chalk.hex(C.blue).bold(` ${lang} `) : "";
|
|
103
|
+
const lblLen = lang ? lang.length + 2 : 0;
|
|
104
|
+
const dashes = Math.max(2, bw2 - lblLen - 1);
|
|
105
|
+
const maxCode = bw2 - gw - 4;
|
|
106
|
+
|
|
107
|
+
pushBlank();
|
|
108
|
+
pushStyled(chalk.hex(C.dim)("┌─") + lbl + chalk.hex(C.dimmer)("─".repeat(dashes)) + chalk.hex(C.dim)("┐"));
|
|
109
|
+
hlLines.forEach((hl_line, i) => {
|
|
110
|
+
const ln = chalk.hex(C.dimmer)(String(i + 1).padStart(gw));
|
|
111
|
+
const raw = lines[i] ?? "";
|
|
112
|
+
const disp = raw.length > maxCode ? hl_line.slice(0, maxCode * 3) + chalk.hex(C.dimmer)("…") : hl_line;
|
|
113
|
+
pushStyled(chalk.hex(C.dim)("│") + chalk.hex(C.dimmer)(" " + ln + " ╎ ") + disp);
|
|
114
|
+
});
|
|
115
|
+
pushStyled(chalk.hex(C.dim)("└" + "─".repeat(bw2 + 1) + "┘"));
|
|
116
|
+
pushBlank();
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Text segment — process line by line ───────────────────
|
|
121
|
+
for (const rawLine of seg.split("\n")) {
|
|
122
|
+
const line = rawLine;
|
|
123
|
+
|
|
124
|
+
// Blank line
|
|
125
|
+
if (!line.trim()) { pushBlank(); continue; }
|
|
126
|
+
|
|
127
|
+
// === Header === (non-standard but model uses it)
|
|
128
|
+
if (/^={3,}\s*.+\s*={3,}$/.test(line)) {
|
|
129
|
+
const t = line.replace(/^=+\s*/, "").replace(/\s*=+$/, "");
|
|
130
|
+
pushBlank();
|
|
131
|
+
pushStyled(chalk.hex(C.blue).bold.underline(t));
|
|
132
|
+
pushBlank();
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ATX headers
|
|
137
|
+
if (/^### /.test(line)) { pushStyled(chalk.hex(C.yellow).bold("◈ " + line.slice(4))); continue; }
|
|
138
|
+
if (/^## /.test(line)) { pushStyled(chalk.hex(C.blue).bold.underline(line.slice(3))); continue; }
|
|
139
|
+
if (/^# /.test(line)) { pushStyled(chalk.hex(C.teal).bold("◉ " + line.slice(2).toUpperCase())); continue; }
|
|
140
|
+
|
|
141
|
+
// HR
|
|
142
|
+
if (/^---+$/.test(line)) { pushStyled(chalk.hex(C.dimmer)("╌".repeat(Math.min(cw, 48)))); continue; }
|
|
143
|
+
|
|
144
|
+
// Blockquote
|
|
145
|
+
if (/^> /.test(line)) {
|
|
146
|
+
const t = line.slice(2);
|
|
147
|
+
wrapPlain(t, cw - 2).forEach((wl, i) =>
|
|
148
|
+
pushStyled(chalk.hex(C.purple)("▎ ") + chalk.hex(C.muted).italic(wl))
|
|
149
|
+
);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Unordered list
|
|
154
|
+
const ulM = line.match(/^(\s*)[*\-+] (.+)$/);
|
|
155
|
+
if (ulM) {
|
|
156
|
+
const [, ind, content] = ulM;
|
|
157
|
+
const indW = ind.length;
|
|
158
|
+
const itemW = cw - indW - 4; // "◆ " = 2 chars visible + 2 padding
|
|
159
|
+
const plain = stripInline(content);
|
|
160
|
+
wrapPlain(plain, itemW).forEach((wl, i) => {
|
|
161
|
+
if (i === 0)
|
|
162
|
+
pushStyled(ind + chalk.hex(C.teal)("◆ ") + chalk.hex(C.white)(applyInline(wl)));
|
|
163
|
+
else
|
|
164
|
+
pushStyled(ind + " " + chalk.hex(C.white)(applyInline(wl)));
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Ordered list
|
|
170
|
+
const olM = line.match(/^(\s*)(\d+)\. (.+)$/);
|
|
171
|
+
if (olM) {
|
|
172
|
+
const [, ind, num, content] = olM;
|
|
173
|
+
const markerW = num.length + 2;
|
|
174
|
+
const itemW = cw - ind.length - markerW;
|
|
175
|
+
const plain = stripInline(content);
|
|
176
|
+
wrapPlain(plain, itemW).forEach((wl, i) => {
|
|
177
|
+
if (i === 0)
|
|
178
|
+
pushStyled(ind + chalk.hex(C.blue)(chalk.bold(num + ".") + " ") + chalk.hex(C.white)(applyInline(wl)));
|
|
179
|
+
else
|
|
180
|
+
pushStyled(ind + " ".repeat(markerW) + chalk.hex(C.white)(applyInline(wl)));
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Plain paragraph — strip inline markup for wrapping, then re-apply
|
|
186
|
+
const plain = stripInline(line);
|
|
187
|
+
wrapPlain(plain, cw).forEach(wl =>
|
|
188
|
+
pushStyled(chalk.hex(C.white)(applyInline(wl)))
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Strip inline markdown to get plain text for accurate wrapping
|
|
197
|
+
function stripInline(s) {
|
|
198
|
+
return s
|
|
199
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // links → text only
|
|
200
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
201
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, "$1")
|
|
202
|
+
.replace(/\*\*(.+?)\*\*/g, "$1")
|
|
203
|
+
.replace(/\*(.+?)\*/g, "$1")
|
|
204
|
+
.replace(/_(.+?)_/g, "$1");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Apply inline markdown styling to already-wrapped line
|
|
208
|
+
function applyInline(s) {
|
|
209
|
+
return s
|
|
210
|
+
// Links → show "text (url truncated)" — URLs not clickable in terminal
|
|
211
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
|
|
212
|
+
const short = url.length > 40 ? url.slice(0, 40) + "…" : url;
|
|
213
|
+
return chalk.hex(C.teal)(text) + chalk.hex(C.dimmer)(" (" + short + ")");
|
|
214
|
+
})
|
|
215
|
+
.replace(/`([^`]+)`/g, (_, c) => chalk.bgHex("#2A2A3E")(chalk.hex(C.orange)(" " + c + " ")))
|
|
216
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => chalk.bold.italic(t))
|
|
217
|
+
.replace(/\*\*(.+?)\*\*/g, (_, t) => chalk.bold(t))
|
|
218
|
+
.replace(/\*(.+?)\*/g, (_, t) => chalk.italic(t))
|
|
219
|
+
.replace(/_(.+?)_/g, (_, t) => chalk.italic(t));
|
|
113
220
|
}
|
|
114
221
|
|
|
115
|
-
//
|
|
116
|
-
|
|
222
|
+
// ─────────────────────────────────────────────────────────────────
|
|
223
|
+
// Box printer — all lines are pre-wrapped, just add border
|
|
224
|
+
// ─────────────────────────────────────────────────────────────────
|
|
225
|
+
function printBox(renderedLines, borderHex, label, extra = "") {
|
|
117
226
|
const W = bw();
|
|
118
|
-
const inner = W - 2;
|
|
119
227
|
const lblLen = vlen(label) + vlen(extra);
|
|
120
228
|
const dashes = Math.max(0, W - lblLen - 1);
|
|
121
229
|
|
|
@@ -124,37 +232,43 @@ function printBox(contentLines, borderHex, label, extra = "") {
|
|
|
124
232
|
chalk.hex(C.dim)("─".repeat(dashes)) + extra + "\n"
|
|
125
233
|
);
|
|
126
234
|
|
|
127
|
-
for (const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
} else {
|
|
132
|
-
wrapText(line || " ", inner).forEach(wl =>
|
|
133
|
-
process.stdout.write(chalk.hex(borderHex)("│ ") + wl + "\n")
|
|
134
|
-
);
|
|
135
|
-
}
|
|
235
|
+
for (const { text, raw } of renderedLines) {
|
|
236
|
+
// raw lines (code blocks, headers with box-drawing) — print as-is
|
|
237
|
+
// non-raw lines — already plain/styled text, just add border
|
|
238
|
+
process.stdout.write(chalk.hex(borderHex)("│ ") + text + "\n");
|
|
136
239
|
}
|
|
137
240
|
|
|
138
241
|
process.stdout.write(chalk.hex(borderHex)("╰" + "─".repeat(W + 1)) + "\n");
|
|
139
242
|
}
|
|
140
243
|
|
|
141
|
-
//
|
|
244
|
+
// ─────────────────────────────────────────────────────────────────
|
|
245
|
+
// Messages
|
|
246
|
+
// ─────────────────────────────────────────────────────────────────
|
|
142
247
|
export function printUser(text) {
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
const
|
|
248
|
+
const W = bw();
|
|
249
|
+
const inner = W - 2;
|
|
250
|
+
const rawLines = text.split("\n");
|
|
251
|
+
const multi = rawLines.length > 1;
|
|
252
|
+
const label = chalk.hex(C.blue).bold(" you ");
|
|
253
|
+
const extra = multi ? chalk.hex(C.muted)(` (${rawLines.length} lines) `) : "";
|
|
254
|
+
|
|
255
|
+
const lines = multi
|
|
256
|
+
? rawLines.flatMap(l => wrapPlain(l, inner).map(wl => ({ text: chalk.hex(C.white)(wl), raw: true })))
|
|
257
|
+
: wrapPlain(text, inner).map(wl => ({ text: chalk.hex(C.white)(wl), raw: true }));
|
|
258
|
+
|
|
147
259
|
printBox(lines, C.blue, label, extra);
|
|
148
260
|
}
|
|
149
261
|
|
|
150
262
|
export function printAssistant(text) {
|
|
151
|
-
const inner
|
|
152
|
-
const
|
|
153
|
-
printBox(
|
|
263
|
+
const inner = bw() - 2;
|
|
264
|
+
const lines = renderMarkdown(text.trimEnd(), inner);
|
|
265
|
+
printBox(lines, C.teal, chalk.hex(C.teal).bold(" gemini "));
|
|
154
266
|
process.stdout.write("\n");
|
|
155
267
|
}
|
|
156
268
|
|
|
157
|
-
//
|
|
269
|
+
// ─────────────────────────────────────────────────────────────────
|
|
270
|
+
// Agent step blocks
|
|
271
|
+
// ─────────────────────────────────────────────────────────────────
|
|
158
272
|
export function printStepHeader(step) {
|
|
159
273
|
const W = bw();
|
|
160
274
|
const label = chalk.hex(C.yellow).bold(" working ");
|
|
@@ -173,7 +287,7 @@ export function printStepFooter() {
|
|
|
173
287
|
export function printToolCall(name, args = {}) {
|
|
174
288
|
const argStr = Object.entries(args).map(([k, v]) => {
|
|
175
289
|
const raw = String(v).replace(/\n/g, "↵");
|
|
176
|
-
const val =
|
|
290
|
+
const val = raw.length > 42 ? raw.slice(0, 42) + "…" : raw;
|
|
177
291
|
return chalk.hex(C.muted)(k + ":") + chalk.hex(C.orange)(val);
|
|
178
292
|
}).join(" ");
|
|
179
293
|
process.stdout.write(
|
|
@@ -199,18 +313,23 @@ export function printToolResult(result) {
|
|
|
199
313
|
if (extra > 0) process.stdout.write(border + chalk.hex(C.dim)(`… +${extra} more lines`) + "\n");
|
|
200
314
|
}
|
|
201
315
|
|
|
202
|
-
//
|
|
316
|
+
// ─────────────────────────────────────────────────────────────────
|
|
317
|
+
// Status
|
|
318
|
+
// ─────────────────────────────────────────────────────────────────
|
|
203
319
|
export function printError(msg) { process.stdout.write("\n" + chalk.hex(C.red)("╳ ") + chalk.hex(C.red).bold("error ") + chalk.hex(C.muted)(msg) + "\n\n"); }
|
|
204
320
|
export function printInfo(msg) { process.stdout.write(chalk.hex(C.dim)("· ") + chalk.hex(C.white)(msg) + "\n"); }
|
|
205
321
|
export function printSuccess(msg) { process.stdout.write(chalk.hex(C.teal)("✓ ") + chalk.hex(C.teal)(msg) + "\n"); }
|
|
206
322
|
export function printWarning(msg) { process.stdout.write(chalk.hex(C.yellow)("⚠ ") + chalk.hex(C.yellow)(msg) + "\n"); }
|
|
207
323
|
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
324
|
+
// ─────────────────────────────────────────────────────────────────
|
|
325
|
+
// Welcome & Help
|
|
326
|
+
// ─────────────────────────────────────────────────────────────────
|
|
327
|
+
export function renderWelcome(memCount = 0, extCount = 0, skillCount = 0) {
|
|
328
|
+
const W = bw();
|
|
211
329
|
const stats = [
|
|
212
|
-
memCount
|
|
213
|
-
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,
|
|
214
333
|
].filter(Boolean).join(" · ");
|
|
215
334
|
return [
|
|
216
335
|
"",
|
|
@@ -224,7 +343,6 @@ export function renderWelcome(memCount = 0, extCount = 0) {
|
|
|
224
343
|
].join("\n");
|
|
225
344
|
}
|
|
226
345
|
|
|
227
|
-
// ─── Help ────────────────────────────────────────────────────────
|
|
228
346
|
export function renderHelp(customCommands = {}) {
|
|
229
347
|
const sep = chalk.hex(C.dimmer)(" " + "─".repeat(Math.min(tw() - 4, 52)));
|
|
230
348
|
const row = (cmd, desc) => " " + chalk.hex(C.blue).bold(cmd.padEnd(24)) + chalk.hex(C.muted)(desc);
|
|
@@ -249,6 +367,15 @@ export function renderHelp(customCommands = {}) {
|
|
|
249
367
|
row("/new /clear", "Reset conversation"),
|
|
250
368
|
row("/model", "Model & config info"),
|
|
251
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"),
|
|
252
379
|
row("/exit /quit", "Exit"), sep,
|
|
253
380
|
chalk.hex(C.dim)(" Ctrl+C interrupt · Ctrl+D exit"), "",
|
|
254
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
|
+
}
|