@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 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 = proxyStatus();
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(`proxy ${px.available}/${px.total} available · ${px.blocked} blocked · ${px.enabled ? "on" : "off"}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikyyofc/gemini-cli",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "AI Agent CLI — native function calling · GEMINI.md context · extensions",
5
5
  "type": "module",
6
6
  "bin": { "gemini": "./index.js" },
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 = FUNCTION_DECLARATIONS.map(t => `- ${t.name}: ${t.description}`).join("\n");
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
- function wrapText(text, maxW) {
17
- if (vlen(text) <= maxW) return [text];
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 adding = cur ? cur + " " + w : w;
23
- if (vlen(adding) <= maxW) { cur = adding; continue; }
23
+ const candidate = cur ? cur + " " + w : w;
24
+ if (candidate.length <= maxW) { cur = candidate; continue; }
24
25
  if (cur) lines.push(cur);
25
- if (vlen(w) > maxW) {
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
- // ─── Syntax highlight ────────────────────────────────────────────
35
- // FIX: applyToRaw() ensures the number regex never matches digits
36
- // inside existing \x00N\x00 placeholder markers (the root cause of
37
- // garbage numbers appearing in rendered code).
36
+ // ─────────────────────────────────────────────────────────────────
37
+ // Syntax highlighting applyToRaw prevents placeholder corruption
38
+ // ─────────────────────────────────────────────────────────────────
38
39
  const KW = {
39
- 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,
40
- 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,
41
- 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,
42
- 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,
43
- 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,
44
- 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,
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", js:"js", typescript:"ts", ts:"ts",
48
- python:"py", py:"py", go:"go", golang:"go",
49
- rust:"rs", rs:"rs", bash:"sh", sh:"sh", shell:"sh", zsh:"sh", fish:"sh",
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 = s => { const id = `\x00${saved.length}\x00`; saved.push(s); return id; };
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, m => save(chalk.hex(C.comment).italic(m)));
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
- // ─── Markdown renderer ───────────────────────────────────────────
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
- let r = text;
76
-
77
- r = r.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
78
- const trimmed = code.trimEnd();
79
- const lines = trimmed.split("\n");
80
- const hl = highlight(trimmed, lang);
81
- const hlLines = hl.split("\n");
82
- const gw = String(lines.length).length;
83
- const bw2 = Math.min(cw + 2, tw() - 6);
84
- const lbl = lang ? chalk.hex(C.blue).bold(` ${lang} `) : "";
85
- const lblLen = lang ? lang.length + 2 : 0;
86
- const dashes = Math.max(2, bw2 - lblLen - 1);
87
- const maxCode = bw2 - gw - 4;
88
-
89
- const top = chalk.hex(C.dim)("┌─") + lbl + chalk.hex(C.dimmer)("─".repeat(dashes)) + chalk.hex(C.dim)("┐");
90
- const bot = chalk.hex(C.dim)("└" + "─".repeat(bw2 + 1) + "┘");
91
- const body = hlLines.map((hl_line, i) => {
92
- const ln = chalk.hex(C.dimmer)(String(i + 1).padStart(gw));
93
- const raw = lines[i] ?? "";
94
- const disp = raw.length > maxCode ? hl_line.slice(0, maxCode * 3) + chalk.hex(C.dimmer)("…") : hl_line;
95
- return chalk.hex(C.dim)("│") + chalk.hex(C.dimmer)(" " + ln + " ╎ ") + disp;
96
- }).join("\n");
97
-
98
- return `\n${top}\n${body}\n${bot}\n`;
99
- });
100
-
101
- r = r.replace(/`([^`\n]+)`/g, (_, c) => chalk.bgHex("#2A2A3E")(chalk.hex(C.orange)(" " + c + " ")));
102
- r = r.replace(/^### (.+)$/gm, (_, t) => "\n" + chalk.hex(C.yellow).bold("◈ " + t));
103
- r = r.replace(/^## (.+)$/gm, (_, t) => "\n" + chalk.hex(C.blue).bold.underline(t));
104
- r = r.replace(/^# (.+)$/gm, (_, t) => "\n" + chalk.hex(C.teal).bold("" + t.toUpperCase()));
105
- r = r.replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => chalk.bold.italic(t));
106
- r = r.replace(/\*\*(.+?)\*\*/g, (_, t) => chalk.bold(t));
107
- r = r.replace(/\*(.+?)\*/g, (_, t) => chalk.italic(t));
108
- r = r.replace(/^> (.+)$/gm, (_, t) => chalk.hex(C.purple)("") + chalk.hex(C.muted).italic(t));
109
- r = r.replace(/^(\s*)[*\-+] (.+)$/gm, (_, i, t) => i + chalk.hex(C.teal)("") + chalk.hex(C.white)(t));
110
- r = r.replace(/^(\s*)(\d+)\. (.+)$/gm, (_, i, n, t) => i + chalk.hex(C.blue)(chalk.bold(n + ".") + " ") + chalk.hex(C.white)(t));
111
- r = r.replace(/^---+$/gm, chalk.hex(C.dimmer)("".repeat(Math.min(cw, 48))));
112
- return r;
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
- // ─── Box printer ─────────────────────────────────────────────────
116
- function printBox(contentLines, borderHex, label, extra = "") {
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 line of contentLines) {
128
- if (line.includes("\x1b[")) {
129
- // Already styled (code blocks, headers) print as-is
130
- process.stdout.write(chalk.hex(borderHex)("│ ") + line + "\n");
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
- // ─── Messages ────────────────────────────────────────────────────
244
+ // ─────────────────────────────────────────────────────────────────
245
+ // Messages
246
+ // ─────────────────────────────────────────────────────────────────
142
247
  export function printUser(text) {
143
- const lines = text.split("\n");
144
- const multi = lines.length > 1;
145
- const label = chalk.hex(C.blue).bold(" you ");
146
- const extra = multi ? chalk.hex(C.muted)(` (${lines.length} lines) `) : "";
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 = bw() - 2;
152
- const rendered = renderMarkdown(text.trimEnd(), inner);
153
- printBox(rendered.split("\n"), C.teal, chalk.hex(C.teal).bold(" gemini "));
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
- // ─── Agent step blocks ───────────────────────────────────────────
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 = vlen(raw) > 42 ? raw.slice(0, 42) + "…" : raw;
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
- // ─── Status ──────────────────────────────────────────────────────
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
- // ─── Welcome ─────────────────────────────────────────────────────
209
- export function renderWelcome(memCount = 0, extCount = 0) {
210
- const W = bw();
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 ? `${memCount} context file${memCount > 1 ? "s" : ""}` : null,
213
- extCount ? `${extCount} extension${extCount > 1 ? "s" : ""}` : null,
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
+ }