@ikyyofc/gemini-cli 1.0.0

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/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # Gemini CLI πŸ€–
2
+
3
+ > AI Agent CLI β€” native function calling Β· GEMINI.md context Β· extension system
4
+
5
+ ```
6
+ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ•—
7
+ β–ˆβ–ˆβ•”β•β•β•β•β• β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘
8
+ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•”β–ˆβ–ˆβ–ˆβ–ˆβ•”β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘
9
+ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β• β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘
10
+ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β•šβ•β• β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘
11
+ β•šβ•β•β•β•β•β• β•šβ•β•β•β•β•β•β•β•šβ•β• β•šβ•β•β•šβ•β•β•šβ•β• β•šβ•β•β•β•β•šβ•β•
12
+ ```
13
+
14
+ ---
15
+
16
+ ## Instalasi
17
+
18
+ ```bash
19
+ npm install
20
+ chmod +x index.js
21
+ npm link # optional: pakai sebagai `gemini` di terminal
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Penggunaan
27
+
28
+ ```bash
29
+ gemini # interactive agent
30
+ gemini "buatkan REST API di ./api" # one-shot
31
+ gemini --system "Kamu senior backend engineer"
32
+ gemini --file ./app.js "jelaskan kode ini"
33
+ gemini --yolo "refactor semua file di src/"
34
+ gemini --chat # plain chat tanpa tools
35
+ ```
36
+
37
+ ---
38
+
39
+ ## GEMINI.md β€” Context Files
40
+
41
+ Buat `GEMINI.md` di lokasi berikut (dimuat hierarki, seperti Gemini CLI asli):
42
+
43
+ | Lokasi | Scope |
44
+ |--------|-------|
45
+ | `~/.gemini/GEMINI.md` | Global semua project |
46
+ | `./GEMINI.md` | Project root (sampai `.git`) |
47
+
48
+ Support import antar file:
49
+ ```md
50
+ @./components/style.md
51
+ @../shared/conventions.md
52
+ ```
53
+
54
+ **Commands:**
55
+ ```
56
+ /memory show β†’ tampilkan semua context yang dimuat
57
+ /memory reload β†’ reload dari disk
58
+ /memory add <text> β†’ append ke ~/.gemini/GEMINI.md
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Extensions
64
+
65
+ Manifest: `~/.gemini/extensions/<name>/gemini-extension.json`
66
+
67
+ ```json
68
+ {
69
+ "name": "my-ext",
70
+ "version": "1.0.0",
71
+ "description": "...",
72
+ "contextFileName": "GEMINI.md",
73
+ "enabled": true,
74
+ "commands": {
75
+ "do-thing": {
76
+ "description": "Does a thing",
77
+ "prompt": "Do this: {{args}}"
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ **Commands:**
84
+ ```
85
+ /ext list
86
+ /ext install /path/to/ext
87
+ /ext install https://github.com/user/repo
88
+ /ext uninstall <name>
89
+ /ext enable / disable <name>
90
+ /ext update <name>
91
+ ```
92
+
93
+ Custom commands dipanggil: `/code-reviewer:review ./src/app.js`
94
+
95
+ ---
96
+
97
+ ## Tools (Native Function Calling)
98
+
99
+ | Tool | Aksi |
100
+ |------|------|
101
+ | `read_file` | Baca file |
102
+ | `write_file` | Tulis/overwrite file |
103
+ | `patch_file` | Edit bagian spesifik |
104
+ | `append_file` | Append ke file |
105
+ | `list_dir` | List direktori |
106
+ | `find_files` | Cari file (glob) |
107
+ | `search_in_files` | Grep dalam file |
108
+ | `run_shell` | Jalankan shell command |
109
+ | `create_dir` | Buat direktori |
110
+ | `delete_file` | Hapus file |
111
+ | `move_file` | Pindah/rename |
112
+ | `get_env` | Info environment |
113
+ | `read_url` | Fetch URL / API |
114
+
115
+ ---
116
+
117
+ ## Struktur
118
+
119
+ ```
120
+ gemini-cli/
121
+ β”œβ”€β”€ index.js ← CLI entry + REPL + commands
122
+ β”œβ”€β”€ GEMINI.md ← Project context (auto-loaded)
123
+ β”œβ”€β”€ package.json
124
+ β”œβ”€β”€ src/
125
+ β”‚ β”œβ”€β”€ gemini.js ← API client (native function calling)
126
+ β”‚ β”œβ”€β”€ tools.js ← functionDeclarations + executor
127
+ β”‚ β”œβ”€β”€ agent.js ← ReAct loop
128
+ β”‚ β”œβ”€β”€ memory.js ← GEMINI.md hierarchy loader
129
+ β”‚ β”œβ”€β”€ extensions.js ← Extension manager
130
+ β”‚ └── renderer.js ← Terminal UI + markdown
131
+ └── extensions/
132
+ └── code-reviewer/
133
+ β”œβ”€β”€ gemini-extension.json
134
+ └── GEMINI.md
135
+
136
+ ~/.gemini/ ← Global config dir
137
+ β”œβ”€β”€ GEMINI.md
138
+ β”œβ”€β”€ extensions/
139
+ └── commands/
140
+ ```
package/index.js ADDED
@@ -0,0 +1,362 @@
1
+ #!/usr/bin/env node
2
+ // ═══════════════════════════════════════════════════════════════
3
+ // Gemini CLI v2 β€” AI Agent
4
+ // Native function calling Β· GEMINI.md context hierarchy
5
+ // Extension system Β· Custom commands Β· /memory Β· /ext
6
+ // ═══════════════════════════════════════════════════════════════
7
+ import readline from "readline";
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import chalk from "chalk";
11
+
12
+ import { chat } from "./src/gemini.js";
13
+ import { runAgentLoop } from "./src/agent.js";
14
+ import {
15
+ loadMemory, buildContextString, memoryShow, memoryAdd, ensureGlobalDir, GLOBAL_DIR
16
+ } from "./src/memory.js";
17
+ import {
18
+ loadExtensions, loadCustomCommands, getExtensionContextDirs,
19
+ installExtension, uninstallExtension, enableExtension, listExtensions, updateExtension
20
+ } from "./src/extensions.js";
21
+ import {
22
+ renderWelcome, renderHelp,
23
+ printUser, printAssistant,
24
+ printError, printInfo, printSuccess, printWarning
25
+ } from "./src/renderer.js";
26
+
27
+ // ─────────────────────────────────────────────────────────────────
28
+ // Bootstrap
29
+ // ─────────────────────────────────────────────────────────────────
30
+ ensureGlobalDir();
31
+
32
+ let extensions = loadExtensions();
33
+ let customCommands = {}; // populated after extensions load
34
+ let memoryLoaded = []; // { file, content }[]
35
+ let memoryContext = null; // concatenated string
36
+
37
+ function reloadAll() {
38
+ extensions = loadExtensions();
39
+ customCommands = {}; // TODO: loadCustomCommands(extensions) β€” needs top-level await
40
+ const extraDirs = getExtensionContextDirs(extensions);
41
+ memoryLoaded = loadMemory(extraDirs);
42
+ memoryContext = buildContextString(memoryLoaded);
43
+ }
44
+ reloadAll();
45
+
46
+ // ─────────────────────────────────────────────────────────────────
47
+ // State
48
+ // ─────────────────────────────────────────────────────────────────
49
+ let conversationHistory = [];
50
+ let sessionSystem = null; // from /system command or --system flag
51
+ let pendingFile = null;
52
+ let pendingFilePath = null;
53
+ let isProcessing = false;
54
+ let agentMode = true;
55
+ let autoApprove = false;
56
+
57
+ /** Full system instruction: memory context + session system */
58
+ function buildSystemInstruction() {
59
+ const parts = [memoryContext, sessionSystem].filter(Boolean);
60
+ return parts.length ? parts.join("\n\n") : null;
61
+ }
62
+
63
+ // ─────────────────────────────────────────────────────────────────
64
+ // Readline
65
+ // ─────────────────────────────────────────────────────────────────
66
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true, historySize: 200 });
67
+
68
+ function setPrompt() {
69
+ const mode = agentMode ? chalk.hex("#4EC9B0")(" ⚑agent") : chalk.hex("#858585")(" πŸ’¬chat");
70
+ const yolo = autoApprove ? chalk.hex("#F44747")(" YOLO") : "";
71
+ const fTag = pendingFile ? chalk.hex("#DCDCAA")(` πŸ“Ž ${path.basename(pendingFilePath)}`) : "";
72
+ const turns = conversationHistory.length > 0 ? chalk.hex("#858585")(` [${Math.ceil(conversationHistory.length/2)}t]`) : "";
73
+ const mem = memoryLoaded.length ? chalk.hex("#C586C0")(` [mem:${memoryLoaded.length}]`) : "";
74
+ rl.setPrompt("\n " + chalk.hex("#569CD6").bold("❯") + mode + yolo + fTag + turns + mem + chalk.hex("#D4D4D4")(" "));
75
+ rl.prompt();
76
+ }
77
+
78
+ // ─────────────────────────────────────────────────────────────────
79
+ // File attachment
80
+ // ─────────────────────────────────────────────────────────────────
81
+ function loadFile(filePath) {
82
+ const p = path.resolve(filePath.trim().replace(/^['"]|['"]$/g, ""));
83
+ if (!fs.existsSync(p)) { printError(`File not found: ${p}`); return; }
84
+ if (fs.statSync(p).size > 20*1024*1024) { printError("File too large (max 20 MB)"); return; }
85
+ pendingFile = fs.readFileSync(p);
86
+ pendingFilePath = p;
87
+ printSuccess(`Attached: ${path.basename(p)} (${(fs.statSync(p).size/1024).toFixed(1)} KB)`);
88
+ printInfo("Will be sent with your next message.");
89
+ }
90
+
91
+ // ─────────────────────────────────────────────────────────────────
92
+ // Command router
93
+ // ─────────────────────────────────────────────────────────────────
94
+ async function handleCommand(input) {
95
+ const tokens = input.trim().split(/\s+/);
96
+ const cmd = tokens[0].toLowerCase();
97
+ const arg = tokens.slice(1).join(" ").trim();
98
+
99
+ // ── /memory ─────────────────────────────────────────────────
100
+ if (cmd === "/memory") {
101
+ const sub = tokens[1]?.toLowerCase();
102
+ if (sub === "show") {
103
+ console.log(memoryShow(memoryLoaded));
104
+ } else if (sub === "reload") {
105
+ reloadAll();
106
+ printSuccess(`Reloaded. ${memoryLoaded.length} context file(s) loaded.`);
107
+ } else if (sub === "add") {
108
+ const text = tokens.slice(2).join(" ");
109
+ if (!text) { printError("Usage: /memory add <text>"); return; }
110
+ const result = memoryAdd(text);
111
+ printSuccess(result);
112
+ reloadAll();
113
+ } else {
114
+ printInfo("Usage: /memory [show|reload|add <text>]");
115
+ }
116
+ return;
117
+ }
118
+
119
+ // ── /ext ────────────────────────────────────────────────────
120
+ if (cmd === "/ext") {
121
+ const sub = tokens[1]?.toLowerCase();
122
+ const name = tokens[2];
123
+
124
+ if (sub === "list") {
125
+ const list = listExtensions();
126
+ if (!list.length) { printInfo("No extensions installed. Use /ext install <source>"); return; }
127
+ console.log("");
128
+ list.forEach(e => {
129
+ const status = e.enabled ? chalk.hex("#4EC9B0")("βœ” enabled") : chalk.hex("#858585")("β—‹ disabled");
130
+ console.log(
131
+ ` ${status} ` +
132
+ chalk.hex("#DCDCAA").bold(e.name) +
133
+ chalk.hex("#858585")(` v${e.version}`) +
134
+ (e.description ? chalk.hex("#858585")(` β€” ${e.description}`) : "")
135
+ );
136
+ });
137
+ console.log("");
138
+
139
+ } else if (sub === "install") {
140
+ if (!arg.slice(arg.indexOf(" ")+1).trim() && !name) { printError("Usage: /ext install <path-or-git-url>"); return; }
141
+ const src = tokens.slice(2).join(" ");
142
+ printInfo(`Installing: ${src} …`);
143
+ const res = await installExtension(src);
144
+ if (res.error) printError(res.error);
145
+ else { printSuccess(`Installed: ${res.name} (${res.path})`); reloadAll(); }
146
+
147
+ } else if (sub === "uninstall") {
148
+ if (!name) { printError("Usage: /ext uninstall <name>"); return; }
149
+ const res = await uninstallExtension(name);
150
+ if (res.error) printError(res.error);
151
+ else { printSuccess(`Uninstalled: ${name}`); reloadAll(); }
152
+
153
+ } else if (sub === "enable") {
154
+ if (!name) { printError("Usage: /ext enable <name>"); return; }
155
+ const res = enableExtension(name, true);
156
+ if (res.error) printError(res.error); else { printSuccess(`Enabled: ${name}`); reloadAll(); }
157
+
158
+ } else if (sub === "disable") {
159
+ if (!name) { printError("Usage: /ext disable <name>"); return; }
160
+ const res = enableExtension(name, false);
161
+ if (res.error) printError(res.error); else { printSuccess(`Disabled: ${name}`); reloadAll(); }
162
+
163
+ } else if (sub === "update") {
164
+ if (!name) { printError("Usage: /ext update <name>"); return; }
165
+ printInfo(`Updating ${name}…`);
166
+ const res = await updateExtension(name);
167
+ if (res.error) printError(res.error); else { printSuccess(`Updated: ${name}`); reloadAll(); }
168
+
169
+ } else {
170
+ printInfo("Usage: /ext [list|install|uninstall|enable|disable|update] [name]");
171
+ }
172
+ return;
173
+ }
174
+
175
+ // ── Built-in commands ────────────────────────────────────────
176
+ switch (cmd) {
177
+ case "/help":
178
+ console.log(renderHelp(customCommands));
179
+ break;
180
+
181
+ case "/new": case "/clear":
182
+ conversationHistory = [];
183
+ pendingFile = null; pendingFilePath = null;
184
+ printInfo("Conversation cleared.");
185
+ break;
186
+
187
+ case "/file":
188
+ if (!arg) { printError("Usage: /file <path>"); break; }
189
+ loadFile(arg);
190
+ break;
191
+
192
+ case "/system":
193
+ if (!arg) { printInfo(`System: ${sessionSystem ?? "(none)"}`); break; }
194
+ sessionSystem = arg;
195
+ printSuccess("Session system instruction set.");
196
+ break;
197
+
198
+ case "/agent":
199
+ agentMode = !agentMode;
200
+ printSuccess(`Mode: ${agentMode ? chalk.hex("#4EC9B0").bold("AGENT") + " (native function calling loop)" : chalk.hex("#858585").bold("CHAT") + " (plain conversation)"}`);
201
+ break;
202
+
203
+ case "/yolo":
204
+ autoApprove = !autoApprove;
205
+ autoApprove ? printWarning("YOLO ON β€” all tool actions auto-approved.") : printSuccess("YOLO OFF β€” confirmations restored.");
206
+ break;
207
+
208
+ case "/history":
209
+ if (!conversationHistory.length) { printInfo("No history."); break; }
210
+ console.log("");
211
+ conversationHistory.forEach((m, i) => {
212
+ const who = m.role === "user" ? chalk.hex("#569CD6")("You") : chalk.hex("#4EC9B0")("Gemini");
213
+ const body = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
214
+ const short = body.slice(0, 100).replace(/\n/g, " ");
215
+ console.log(chalk.hex("#858585")(` [${i+1}] `) + who + chalk.hex("#858585")(": ") + chalk.hex("#D4D4D4")(short));
216
+ });
217
+ console.log("");
218
+ break;
219
+
220
+ case "/export":
221
+ if (!arg) { printError("Usage: /export <file.json>"); break; }
222
+ fs.writeFileSync(path.resolve(arg), JSON.stringify({ exported_at: new Date().toISOString(), mode: agentMode ? "agent":"chat", system: buildSystemInstruction(), messages: conversationHistory }, null, 2));
223
+ printSuccess(`Exported to ${arg}`);
224
+ break;
225
+
226
+ case "/cwd":
227
+ printInfo(`Working directory: ${process.cwd()}`);
228
+ break;
229
+
230
+ case "/cd":
231
+ if (!arg) { printError("Usage: /cd <path>"); break; }
232
+ try { process.chdir(path.resolve(arg)); printSuccess(`Changed to: ${process.cwd()}`); } catch (e) { printError(e.message); }
233
+ break;
234
+
235
+ case "/model":
236
+ printInfo("Model : gemini-pro-latest");
237
+ printInfo("Provider : Gemini (Firebase Cloud Function)");
238
+ printInfo("Tool calling : NATIVE (functionDeclarations / functionCall / functionResponse)");
239
+ printInfo("Thinking : HIGH | Temperature: 0 | Safety: BLOCK_NONE");
240
+ printInfo(`Agent mode : ${agentMode ? "ON" : "OFF"}`);
241
+ printInfo(`Auto-approve : ${autoApprove ? "ON (YOLO)" : "OFF"}`);
242
+ printInfo(`Memory files : ${memoryLoaded.length} | Extensions: ${extensions.length}`);
243
+ printInfo(`Global config: ${GLOBAL_DIR}`);
244
+ break;
245
+
246
+ case "/exit": case "/quit":
247
+ printInfo("Goodbye! πŸ‘‹");
248
+ rl.close(); process.exit(0);
249
+ break;
250
+
251
+ default: {
252
+ // Check if it's a custom command: /namespace:cmd [args]
253
+ const customKey = cmd.slice(1); // strip leading /
254
+ if (customKey.includes(":") && customCommands[customKey]) {
255
+ const c = customCommands[customKey];
256
+ const msg = (c.prompt ?? "").replace("{{args}}", arg);
257
+ if (msg) await sendMessage(msg);
258
+ else printError(`Command "${customKey}" has no prompt defined.`);
259
+ } else {
260
+ printError(`Unknown command: ${cmd} (type /help)`);
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ // ─────────────────────────────────────────────────────────────────
267
+ // Send message
268
+ // ─────────────────────────────────────────────────────────────────
269
+ async function sendMessage(userText) {
270
+ if (!userText.trim()) return;
271
+ printUser(userText + (pendingFile ? chalk.hex("#DCDCAA")(` [πŸ“Ž ${path.basename(pendingFilePath)}]`) : ""));
272
+
273
+ const sysInstruction = buildSystemInstruction();
274
+
275
+ if (agentMode) {
276
+ const result = await runAgentLoop(userText, conversationHistory, {
277
+ systemInstruction: sysInstruction,
278
+ autoApprove,
279
+ maxIterations: 40,
280
+ });
281
+ if (result?.finalResponse) {
282
+ conversationHistory.push({ role: "user", content: userText });
283
+ conversationHistory.push({ role: "assistant", content: result.finalResponse });
284
+ }
285
+ } else {
286
+ const { default: ora } = await import("ora");
287
+ const spinner = ora({ text: chalk.hex("#858585")(" Thinking…"), spinner: "dots12", color: "cyan", prefixText: " " }).start();
288
+ const msgs = [];
289
+ if (sysInstruction) msgs.push({ role: "system", content: sysInstruction });
290
+ msgs.push(...conversationHistory, { role: "user", content: userText });
291
+ try {
292
+ const t0 = Date.now();
293
+ const reply = await chat(msgs, pendingFile || null);
294
+ spinner.succeed(chalk.hex("#4EC9B0")("Done") + chalk.hex("#858585")(` (${((Date.now()-t0)/1000).toFixed(1)}s)`));
295
+ conversationHistory.push({ role: "user", content: userText }, { role: "assistant", content: reply });
296
+ printAssistant(reply);
297
+ } catch (err) {
298
+ spinner.fail(chalk.hex("#F44747")("Failed"));
299
+ printError(err.message);
300
+ }
301
+ }
302
+
303
+ pendingFile = null; pendingFilePath = null;
304
+ }
305
+
306
+ // ─────────────────────────────────────────────────────────────────
307
+ // Main
308
+ // ─────────────────────────────────────────────────────────────────
309
+ async function main() {
310
+ process.stdout.write("\x1Bc");
311
+ console.log(renderWelcome(memoryLoaded.length, extensions.length, Object.keys(customCommands).length));
312
+
313
+ // Parse flags
314
+ const argv = process.argv.slice(2);
315
+ const positional = [];
316
+ for (let i = 0; i < argv.length; i++) {
317
+ const a = argv[i];
318
+ if (a === "--system" && argv[i+1]) { sessionSystem = argv[++i]; printSuccess(`System: "${sessionSystem}"`); }
319
+ else if (a === "--file" && argv[i+1]) { loadFile(argv[++i]); }
320
+ else if (a === "--yolo") { autoApprove = true; printWarning("YOLO mode ON."); }
321
+ else if (a === "--chat") { agentMode = false; }
322
+ else if (!a.startsWith("--")) { positional.push(a); }
323
+ }
324
+
325
+ // One-shot
326
+ if (positional.length > 0) {
327
+ await sendMessage(positional.join(" "));
328
+ process.exit(0);
329
+ }
330
+
331
+ // Print mode info
332
+ printInfo(
333
+ `Mode: ${agentMode
334
+ ? chalk.hex("#4EC9B0").bold("AGENT") + chalk.hex("#858585")(" (native function calling Β· filesystem Β· shell Β· web)")
335
+ : chalk.hex("#858585").bold("CHAT")
336
+ } | ${chalk.hex("#CE9178")("/agent")} toggle | ${chalk.hex("#CE9178")("/yolo")} skip confirms | ${chalk.hex("#CE9178")("/help")} commands`
337
+ );
338
+ if (memoryContext) {
339
+ printInfo(`Context: ${chalk.hex("#C586C0")(memoryLoaded.length + " GEMINI.md file(s) loaded")} β€” use ${chalk.hex("#CE9178")("/memory show")} to inspect`);
340
+ }
341
+ console.log("");
342
+
343
+ setPrompt();
344
+
345
+ rl.on("line", async line => {
346
+ const input = line.trim();
347
+ if (!input) { setPrompt(); return; }
348
+ if (isProcessing) { printWarning("Still processing…"); setPrompt(); return; }
349
+ isProcessing = true; rl.pause();
350
+ try {
351
+ if (input.startsWith("/")) await handleCommand(input);
352
+ else await sendMessage(input);
353
+ } finally {
354
+ isProcessing = false; rl.resume(); setPrompt();
355
+ }
356
+ });
357
+
358
+ rl.on("close", () => { console.log("\n" + chalk.hex("#858585")(" Session ended.\n")); process.exit(0); });
359
+ rl.on("SIGINT", () => { if (isProcessing) { printWarning("Ctrl+C again to force quit."); return; } rl.close(); });
360
+ }
361
+
362
+ main().catch(err => { console.error(chalk.red("\n Fatal:"), err.message); process.exit(1); });
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@ikyyofc/gemini-cli",
3
+ "version": "1.0.0",
4
+ "description": "AI CLI Agent powered by Gemini β€” your own terminal assistant",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "gemini": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js",
12
+ "dev": "node --watch index.js"
13
+ },
14
+ "dependencies": {
15
+ "axios": "^1.7.9",
16
+ "chalk": "^5.3.0",
17
+ "cli-spinners": "^2.9.2",
18
+ "file-type": "^19.6.0",
19
+ "ora": "^8.1.1",
20
+ "readline": "^1.3.0",
21
+ "marked": "^12.0.0",
22
+ "marked-terminal": "^7.1.0",
23
+ "minimist": "^1.2.8"
24
+ }
25
+ }
package/src/agent.js ADDED
@@ -0,0 +1,141 @@
1
+ // src/agent.js β€” ReAct agent loop using NATIVE Gemini function calling
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import { callGemini } from "./gemini.js";
5
+ import { GEMINI_TOOLS, executeTool } from "./tools.js";
6
+ import { printAssistant, printError, printWarning } from "./renderer.js";
7
+
8
+ /**
9
+ * Run the agent loop until the model gives a final text answer (no more tool calls).
10
+ *
11
+ * Native function calling flow:
12
+ * 1. Send contents + tools (functionDeclarations) to Gemini
13
+ * 2. Response parts may contain: { functionCall: { name, args } }
14
+ * 3. Execute the tool β†’ get result
15
+ * 4. Append model turn (with functionCall) + user turn (with functionResponse)
16
+ * 5. Repeat until response has only text parts β†’ done
17
+ */
18
+ export async function runAgentLoop(userMessage, history, {
19
+ systemInstruction = null,
20
+ autoApprove = false,
21
+ maxIterations = 40,
22
+ } = {}) {
23
+
24
+ // Build initial message list
25
+ const messages = [
26
+ ...history,
27
+ { role: "user", content: userMessage }
28
+ ];
29
+
30
+ let iteration = 0;
31
+
32
+ while (iteration < maxIterations) {
33
+ iteration++;
34
+
35
+ // ── Call Gemini API with tools ──────────────────────────
36
+ const spinner = ora({
37
+ text: chalk.hex("#858585")(iteration === 1 ? " Thinking…" : ` Step ${iteration}…`),
38
+ spinner: "dots12",
39
+ color: "cyan",
40
+ prefixText: " "
41
+ }).start();
42
+
43
+ let parts;
44
+ try {
45
+ const res = await callGemini({ messages, tools: GEMINI_TOOLS, systemInstruction });
46
+ parts = res.parts;
47
+ spinner.stop();
48
+ clearLine();
49
+ } catch (err) {
50
+ spinner.fail(chalk.hex("#F44747")("API error"));
51
+ printError(err.message);
52
+ return null;
53
+ }
54
+
55
+ // ── Separate text parts from functionCall parts ─────────
56
+ const textParts = parts.filter(p => p.text != null);
57
+ const callParts = parts.filter(p => p.functionCall != null);
58
+
59
+ // Print any accompanying text (model thinking aloud)
60
+ const textContent = textParts.map(p => p.text).join("").trim();
61
+ if (textContent && callParts.length > 0) {
62
+ process.stdout.write(
63
+ "\n" + chalk.hex("#858585").italic(
64
+ " πŸ’­ " + textContent.replace(/\n/g, "\n ")
65
+ ) + "\n\n"
66
+ );
67
+ }
68
+
69
+ // ── No tool calls β†’ final answer ────────────────────────
70
+ if (callParts.length === 0) {
71
+ const final = textParts.map(p => p.text).join("").trim();
72
+ if (final) printAssistant(final);
73
+ return { finalResponse: final, iterations: iteration };
74
+ }
75
+
76
+ // ── Execute each tool call ───────────────────────────────
77
+ // Append the model's turn (contains functionCall parts) to history
78
+ messages.push({ role: "model", parts });
79
+
80
+ // Build the functionResponse turn
81
+ const responseParts = [];
82
+
83
+ for (const part of callParts) {
84
+ const { name, args } = part.functionCall;
85
+ printToolCall(name, args);
86
+
87
+ const result = await executeTool(name, args ?? {}, { autoApprove });
88
+ printToolResult(result, name);
89
+
90
+ responseParts.push({
91
+ functionResponse: {
92
+ name,
93
+ response: result // the entire result object goes here
94
+ }
95
+ });
96
+ }
97
+
98
+ // Append user turn with all function responses
99
+ messages.push({ role: "user", parts: responseParts });
100
+ }
101
+
102
+ printWarning(`Max iterations (${maxIterations}) reached.`);
103
+ return { finalResponse: null, iterations: iteration };
104
+ }
105
+
106
+ // ─────────────────────────────────────────────────────────────────
107
+ // Terminal rendering helpers for tool calls
108
+ // ─────────────────────────────────────────────────────────────────
109
+ function printToolCall(name, args = {}) {
110
+ const preview = Object.entries(args)
111
+ .map(([k, v]) => {
112
+ const s = String(v);
113
+ return chalk.hex("#9CDCFE")(k) + chalk.hex("#858585")(":") +
114
+ chalk.hex("#CE9178")(s.length > 60 ? s.slice(0, 60) + "…" : s);
115
+ })
116
+ .join(" ");
117
+
118
+ process.stdout.write(
119
+ chalk.hex("#DCDCAA")(" βš™ ") +
120
+ chalk.hex("#569CD6").bold(name) +
121
+ (preview ? chalk.hex("#858585")(" (" + preview + ")") : "") + "\n"
122
+ );
123
+ }
124
+
125
+ function printToolResult(result, name) {
126
+ const text = typeof result === "object" ? (result.result ?? result.error ?? JSON.stringify(result)) : String(result);
127
+ const isErr = typeof result === "object" && result.error;
128
+ const color = isErr ? chalk.hex("#F44747") : chalk.hex("#6A9955");
129
+ const lines = text.split("\n").slice(0, 15);
130
+ const more = text.split("\n").length > 15 ? `\n … (+${text.split("\n").length - 15} lines)` : "";
131
+
132
+ process.stdout.write(
133
+ color(" β”” ") +
134
+ chalk.hex("#858585")(lines.join("\n ")) +
135
+ more + "\n\n"
136
+ );
137
+ }
138
+
139
+ function clearLine() {
140
+ if (process.stdout.clearLine) { process.stdout.clearLine(0); process.stdout.cursorTo(0); }
141
+ }