@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 +140 -0
- package/index.js +362 -0
- package/package.json +25 -0
- package/src/agent.js +141 -0
- package/src/extensions.js +195 -0
- package/src/gemini.js +119 -0
- package/src/memory.js +102 -0
- package/src/renderer.js +202 -0
- package/src/tools.js +361 -0
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
|
+
}
|