@ikyyofc/gemini-cli 3.0.1 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -24,11 +24,17 @@ import {
24
24
  } from "./src/input.js";
25
25
  import { setupGlobalProxy, proxyStatus, setProxyEnabled } from "./src/utils/proxy.js";
26
26
  import { Spinner } from "./src/utils/spinner.js";
27
+ import {
28
+ listInstalledSkills, installSkill, removeSkillNpx,
29
+ findSkills, listNpxSkills, updateSkill, initSkill,
30
+ ensureSkillsDirs, loadSkills,
31
+ } from "./src/skills.js";
27
32
 
28
33
  // ─────────────────────────────────────────────────────────────────
29
34
  // Bootstrap
30
35
  // ─────────────────────────────────────────────────────────────────
31
36
  ensureGlobalDir();
37
+ ensureSkillsDirs();
32
38
  setupGlobalProxy(); // aktifkan rotasi proxy sebelum request apapun
33
39
 
34
40
  let extensions = loadExtensions();
@@ -231,6 +237,136 @@ async function handleCommand(input) {
231
237
  printInfo("conversation cleared");
232
238
  break;
233
239
 
240
+ case "/skill": case "/skills": {
241
+ const sub = tokens[1]?.toLowerCase();
242
+ const rest = tokens.slice(2).join(" ").trim();
243
+
244
+ // ── /skill list ──────────────────────────────────────
245
+ if (!sub || sub === "list" || sub === "ls") {
246
+ // Quick disk scan (instant, no npx)
247
+ const disk = listInstalledSkills();
248
+ if (!disk.length) {
249
+ printInfo("no skills installed");
250
+ printInfo("browse: https://skills.sh · install: /skill add <owner/repo>");
251
+ break;
252
+ }
253
+ console.log("");
254
+ disk.forEach(s => {
255
+ const scope = s.global ? chalk.dim("global") : chalk.hex("#4EC9B0")("project");
256
+ console.log(
257
+ " " + chalk.hex("#FFD080").bold(s.name.padEnd(30)) +
258
+ chalk.dim(s.slug.padEnd(26)) + scope
259
+ );
260
+ });
261
+ console.log(chalk.dim(`\n ${disk.length} skill(s) active in agent context\n`));
262
+ break;
263
+ }
264
+
265
+ // ── /skill add <source> [--global] [--skill <name>] ──
266
+ if (sub === "add" || sub === "install") {
267
+ const isGlobal = rest.includes("--global") || rest.includes("-g");
268
+ const skillMatch = rest.match(/--skill\s+"?([^"]+)"?/);
269
+ const skillName = skillMatch?.[1] ?? null;
270
+ const all = rest.includes("--all");
271
+ const source = rest
272
+ .replace(/--global|-g|--skill\s+"?[^"]*"?|--all/g, "")
273
+ .trim();
274
+
275
+ if (!source) { printError("usage: /skill add <owner/repo> [--global] [--skill <name>] [--all]"); break; }
276
+
277
+ const sp = new Spinner();
278
+ sp.start(`npx skills add ${source}…`, "#FFD080");
279
+ try {
280
+ const { output } = await installSkill(source, { global: isGlobal, skill: skillName, all });
281
+ sp.stop();
282
+ // Print npx output
283
+ if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
284
+ printSuccess("skill installed — active on next message");
285
+ } catch (e) {
286
+ sp.fail(e.message.split("\n")[0]);
287
+ printError(e.message.split("\n")[0]);
288
+ }
289
+ break;
290
+ }
291
+
292
+ // ── /skill remove <slug> ──────────────────────────────
293
+ if (sub === "remove" || sub === "rm") {
294
+ if (!rest) { printError("usage: /skill remove <slug>"); break; }
295
+ const sp = new Spinner();
296
+ sp.start(`removing ${rest}…`, "#FF9060");
297
+ try {
298
+ const { output } = await removeSkillNpx(rest);
299
+ sp.stop();
300
+ if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
301
+ printSuccess(`removed: ${rest}`);
302
+ } catch (e) {
303
+ sp.fail(e.message.split("\n")[0]);
304
+ printError(e.message.split("\n")[0]);
305
+ }
306
+ break;
307
+ }
308
+
309
+ // ── /skill find / search <query> ─────────────────────
310
+ if (sub === "find" || sub === "search") {
311
+ if (!rest) { printError("usage: /skill find <query>"); break; }
312
+ const sp = new Spinner();
313
+ sp.start(`searching "${rest}"…`, "#4A9EFF");
314
+ try {
315
+ const output = await findSkills(rest);
316
+ sp.stop();
317
+ if (output) {
318
+ console.log("");
319
+ output.split("\n").forEach(l => console.log(" " + l));
320
+ console.log("");
321
+ } else {
322
+ printInfo("no results — try a different query");
323
+ }
324
+ } catch (e) {
325
+ sp.fail(e.message.split("\n")[0]);
326
+ printError(e.message.split("\n")[0]);
327
+ }
328
+ break;
329
+ }
330
+
331
+ // ── /skill update [slug] ──────────────────────────────
332
+ if (sub === "update") {
333
+ const sp = new Spinner();
334
+ sp.start(rest ? `updating ${rest}…` : "updating all skills…", "#4A9EFF");
335
+ try {
336
+ const { output } = await updateSkill(rest);
337
+ sp.stop();
338
+ if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
339
+ printSuccess("updated");
340
+ } catch (e) {
341
+ sp.fail(e.message.split("\n")[0]);
342
+ printError(e.message.split("\n")[0]);
343
+ }
344
+ break;
345
+ }
346
+
347
+ // ── /skill init [name] ────────────────────────────────
348
+ if (sub === "init") {
349
+ const sp = new Spinner();
350
+ sp.start("creating SKILL.md template…", "#4A9EFF");
351
+ try {
352
+ const { output } = await initSkill(rest);
353
+ sp.stop();
354
+ if (output) output.split("\n").filter(Boolean).forEach(l => printInfo(l));
355
+ printSuccess("SKILL.md created");
356
+ } catch (e) {
357
+ sp.fail(e.message.split("\n")[0]);
358
+ printError(e.message.split("\n")[0]);
359
+ }
360
+ break;
361
+ }
362
+
363
+ printInfo("usage: /skill [list | add | remove | find | update | init]");
364
+ printInfo(" /skill add vercel-labs/agent-skills");
365
+ printInfo(" /skill add anthropics/skills --skill frontend-design");
366
+ printInfo(" /skill add owner/repo --global");
367
+ break;
368
+ }
369
+
234
370
  case "/file":
235
371
  if (!arg) { printError("usage: /file <path>"); break; }
236
372
  attachFile(arg); break;
@@ -282,12 +418,14 @@ async function handleCommand(input) {
282
418
  break;
283
419
 
284
420
  case "/model": {
285
- const px = proxyStatus();
421
+ const px = proxyStatus();
422
+ const sk = loadSkills();
286
423
  printInfo(`model gemini-pro-latest`);
287
424
  printInfo(`tools ${agentMode ? "on (native function calling)" : "off"}`);
288
425
  printInfo(`yolo ${autoApprove ? "on" : "off"}`);
289
426
  printInfo(`memory ${memoryLoaded.length} file(s) · extensions: ${extensions.length}`);
290
- printInfo(`proxy ${px.available}/${px.total} available · ${px.blocked} blocked · ${px.enabled ? "on" : "off"}`);
427
+ printInfo(`skills ${sk.length} active · .agents/ + ~/.gemini/agents/`);
428
+ printInfo(`proxy ${px.available}/${px.total} available · ${px.enabled ? "on" : "off"}`);
291
429
  printInfo(`config ${GLOBAL_DIR}`);
292
430
  break;
293
431
  }
@@ -324,7 +462,7 @@ async function handleCommand(input) {
324
462
  // ─────────────────────────────────────────────────────────────────
325
463
  async function main() {
326
464
  process.stdout.write("\x1Bc");
327
- console.log(renderWelcome(memoryLoaded.length, extensions.length));
465
+ console.log(renderWelcome(memoryLoaded.length, extensions.length, loadSkills().length));
328
466
 
329
467
  const argv = process.argv.slice(2);
330
468
  const positional = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikyyofc/gemini-cli",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
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
@@ -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 ? `${memCount} context file${memCount > 1 ? "s" : ""}` : null,
331
- 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,
332
333
  ].filter(Boolean).join(" · ");
333
334
  return [
334
335
  "",
@@ -366,6 +367,15 @@ export function renderHelp(customCommands = {}) {
366
367
  row("/new /clear", "Reset conversation"),
367
368
  row("/model", "Model & config info"),
368
369
  row("/proxy [on|off]", "Proxy rotation status/toggle"),
370
+ " " + sep,
371
+ chalk.hex(C.yellow).bold(" Skills · skills.sh"),
372
+ " " + sep,
373
+ row("/skill list", "List installed skills"),
374
+ row("/skill add <id>", "Install from skills.sh (--global for global)"),
375
+ row("/skill remove <n>", "Uninstall a skill"),
376
+ row("/skill search <q>", "Search skills.sh registry"),
377
+ row("/skill browse", "Browse top skills (trending/hot)"),
378
+ row("/skill info <id>", "Show skill details"),
369
379
  row("/exit /quit", "Exit"), sep,
370
380
  chalk.hex(C.dim)(" Ctrl+C interrupt · Ctrl+D exit"), "",
371
381
  ];
package/src/skills.js ADDED
@@ -0,0 +1,189 @@
1
+ // src/skills.js — Skills manager via npx skills CLI
2
+ // All install/search/list/remove operations delegate to "npx skills"
3
+ // which is the official skills.sh CLI (github.com/vercel-labs/skills)
4
+ //
5
+ // Skills directories (Gemini agent convention from skills.sh):
6
+ // Project: ./.gemini/skills/ (committed with project)
7
+ // Global: ~/.gemini/skills/ (across all projects)
8
+ // Custom: ./.agents/ (manual / local skills)
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import os from "os";
12
+ import { promisify } from "util";
13
+ import { exec } from "child_process";
14
+
15
+ const execAsync = promisify(exec);
16
+
17
+ // Directories where npx skills installs for Gemini agent
18
+ const GLOBAL_SKILLS_DIR = path.join(os.homedir(), ".gemini", "skills");
19
+ const PROJECT_SKILLS_DIR = () => path.join(process.cwd(), ".gemini", "skills");
20
+ const CUSTOM_SKILLS_DIR = () => path.join(process.cwd(), ".agents");
21
+
22
+ export function ensureSkillsDirs() {
23
+ fs.mkdirSync(GLOBAL_SKILLS_DIR, { recursive: true });
24
+ }
25
+
26
+ // ─────────────────────────────────────────────────────────────────
27
+ // Run npx skills with timeout
28
+ // ─────────────────────────────────────────────────────────────────
29
+ async function npxSkills(args, cwd = process.cwd(), timeout = 120_000) {
30
+ const cmd = `npx --yes skills ${args}`;
31
+ const { stdout, stderr } = await execAsync(cmd, {
32
+ cwd,
33
+ timeout,
34
+ env: {
35
+ ...process.env,
36
+ // Disable telemetry analytics
37
+ DISABLE_TELEMETRY: "1",
38
+ // Force non-interactive / no color for parsing
39
+ NO_COLOR: "1",
40
+ CI: "1",
41
+ },
42
+ });
43
+ return { stdout: stdout.trim(), stderr: stderr.trim() };
44
+ }
45
+
46
+ // ─────────────────────────────────────────────────────────────────
47
+ // Install — npx skills add <source> -a gemini -y [--global]
48
+ // ─────────────────────────────────────────────────────────────────
49
+ export async function installSkill(source, opts = {}) {
50
+ const {
51
+ global = false,
52
+ skill = null, // specific skill name (--skill flag)
53
+ all = false, // install all skills from repo
54
+ } = opts;
55
+
56
+ let args = `add ${source} -a gemini -y --copy`;
57
+ if (global) args += " -g";
58
+ if (skill) args += ` --skill "${skill}"`;
59
+ if (all) args += " --skill '*'";
60
+
61
+ const { stdout, stderr } = await npxSkills(args);
62
+ return { output: stdout || stderr };
63
+ }
64
+
65
+ // ─────────────────────────────────────────────────────────────────
66
+ // List installed — npx skills list
67
+ // ─────────────────────────────────────────────────────────────────
68
+ export async function listNpxSkills(global = false) {
69
+ const args = global ? "list -g" : "list";
70
+ const { stdout } = await npxSkills(args);
71
+ return stdout;
72
+ }
73
+
74
+ // ─────────────────────────────────────────────────────────────────
75
+ // Search — npx skills find <query>
76
+ // ─────────────────────────────────────────────────────────────────
77
+ export async function findSkills(query) {
78
+ const { stdout } = await npxSkills(`find ${JSON.stringify(query)}`);
79
+ return stdout;
80
+ }
81
+
82
+ // ─────────────────────────────────────────────────────────────────
83
+ // Remove — npx skills remove <slug>
84
+ // ─────────────────────────────────────────────────────────────────
85
+ export async function removeSkillNpx(slug) {
86
+ const { stdout, stderr } = await npxSkills(`remove ${slug} -y`);
87
+ return { output: stdout || stderr };
88
+ }
89
+
90
+ // ─────────────────────────────────────────────────────────────────
91
+ // Update — npx skills update [slug]
92
+ // ─────────────────────────────────────────────────────────────────
93
+ export async function updateSkill(slug = "") {
94
+ const args = slug ? `update ${slug} -y` : "update -y";
95
+ const { stdout, stderr } = await npxSkills(args);
96
+ return { output: stdout || stderr };
97
+ }
98
+
99
+ // ─────────────────────────────────────────────────────────────────
100
+ // Init — npx skills init [name] (creates a SKILL.md template)
101
+ // ─────────────────────────────────────────────────────────────────
102
+ export async function initSkill(name = "") {
103
+ const args = name ? `init "${name}"` : "init";
104
+ const { stdout, stderr } = await npxSkills(args);
105
+ return { output: stdout || stderr };
106
+ }
107
+
108
+ // ─────────────────────────────────────────────────────────────────
109
+ // Load skills for agent context injection
110
+ // Scans all skill directories and reads SKILL.md files
111
+ // ─────────────────────────────────────────────────────────────────
112
+ export function loadSkills() {
113
+ const skills = [];
114
+ const seen = new Set();
115
+
116
+ const scanDir = (dir, scope) => {
117
+ if (!fs.existsSync(dir)) return;
118
+
119
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
120
+ for (const entry of entries) {
121
+ if (!entry.isDirectory()) continue;
122
+
123
+ const skillDir = path.join(dir, entry.name);
124
+ const skillMd = path.join(skillDir, "SKILL.md");
125
+ if (!fs.existsSync(skillMd)) continue;
126
+
127
+ const key = entry.name;
128
+ if (seen.has(key)) continue; // project takes priority over global
129
+ seen.add(key);
130
+
131
+ const content = fs.readFileSync(skillMd, "utf8");
132
+
133
+ // Parse optional frontmatter name from SKILL.md
134
+ const nameMatch = content.match(/^#\s+(.+)$/m);
135
+ const name = nameMatch?.[1]?.trim() ?? entry.name;
136
+
137
+ skills.push({ name, slug: entry.name, content, scope, path: skillDir });
138
+ }
139
+ };
140
+
141
+ // Priority: project (.gemini/skills/) > custom (.agents/) > global (~/.gemini/skills/)
142
+ scanDir(PROJECT_SKILLS_DIR(), "project");
143
+ scanDir(CUSTOM_SKILLS_DIR(), "custom");
144
+ scanDir(GLOBAL_SKILLS_DIR, "global");
145
+
146
+ return skills;
147
+ }
148
+
149
+ // ─────────────────────────────────────────────────────────────────
150
+ // Build skills section injected into agent system prompt
151
+ // ─────────────────────────────────────────────────────────────────
152
+ export function buildSkillsPrompt(skills) {
153
+ if (!skills.length) return null;
154
+
155
+ const sections = skills.map(s =>
156
+ `### Skill: ${s.name}\n${s.content.trim()}`
157
+ ).join("\n\n---\n\n");
158
+
159
+ return `## INSTALLED SKILLS\n\nApply the following skills when relevant to the current task:\n\n${sections}`;
160
+ }
161
+
162
+ // ─────────────────────────────────────────────────────────────────
163
+ // Quick disk check — list installed skills without running npx
164
+ // ─────────────────────────────────────────────────────────────────
165
+ export function listInstalledSkills() {
166
+ const result = [];
167
+
168
+ const scanDir = (dir, isGlobal) => {
169
+ if (!fs.existsSync(dir)) return;
170
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
171
+ if (!entry.isDirectory()) continue;
172
+ const skillDir = path.join(dir, entry.name);
173
+ if (!fs.existsSync(path.join(skillDir, "SKILL.md"))) continue;
174
+ const content = fs.readFileSync(path.join(skillDir, "SKILL.md"), "utf8");
175
+ const nameMatch = content.match(/^#\s+(.+)$/m);
176
+ result.push({
177
+ slug: entry.name,
178
+ name: nameMatch?.[1]?.trim() ?? entry.name,
179
+ global: isGlobal,
180
+ dir: skillDir,
181
+ });
182
+ }
183
+ };
184
+
185
+ scanDir(PROJECT_SKILLS_DIR(), false);
186
+ scanDir(CUSTOM_SKILLS_DIR(), false);
187
+ scanDir(GLOBAL_SKILLS_DIR, true);
188
+ return result;
189
+ }