@ikyyofc/gemini-cli 3.0.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js 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.1",
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
@@ -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,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
+ }