@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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// src/extensions.js — Extension manager (mirrors Gemini CLI extension system)
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
import { exec } from "child_process";
|
|
7
|
+
import { GLOBAL_DIR, CONTEXT_FILENAME, readWithImports } from "./memory.js";
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
const EXT_DIR = path.join(GLOBAL_DIR, "extensions");
|
|
11
|
+
const CMD_DIR = path.join(GLOBAL_DIR, "commands");
|
|
12
|
+
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────
|
|
14
|
+
// Extension manifest schema (gemini-extension.json)
|
|
15
|
+
// {
|
|
16
|
+
// "name": "my-ext",
|
|
17
|
+
// "version": "1.0.0",
|
|
18
|
+
// "description": "...",
|
|
19
|
+
// "contextFileName": "GEMINI.md", // optional
|
|
20
|
+
// "commands": { // optional custom commands
|
|
21
|
+
// "cmd-name": {
|
|
22
|
+
// "description": "...",
|
|
23
|
+
// "prompt": "Do {{args}} for me"
|
|
24
|
+
// }
|
|
25
|
+
// },
|
|
26
|
+
// "enabled": true
|
|
27
|
+
// }
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export function getExtensionDir() { return EXT_DIR; }
|
|
31
|
+
export function getCommandDir() { return CMD_DIR; }
|
|
32
|
+
|
|
33
|
+
/** Load all enabled extensions, return their manifests + context dirs */
|
|
34
|
+
export function loadExtensions() {
|
|
35
|
+
fs.mkdirSync(EXT_DIR, { recursive: true });
|
|
36
|
+
const extensions = [];
|
|
37
|
+
|
|
38
|
+
for (const name of fs.readdirSync(EXT_DIR)) {
|
|
39
|
+
const extPath = path.join(EXT_DIR, name);
|
|
40
|
+
const manifest = path.join(extPath, "gemini-extension.json");
|
|
41
|
+
if (!fs.existsSync(manifest)) continue;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const meta = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
45
|
+
if (meta.enabled === false) continue; // disabled
|
|
46
|
+
|
|
47
|
+
const contextFile = meta.contextFileName ?? CONTEXT_FILENAME;
|
|
48
|
+
const contextPath = path.join(extPath, contextFile);
|
|
49
|
+
|
|
50
|
+
extensions.push({
|
|
51
|
+
name: meta.name ?? name,
|
|
52
|
+
version: meta.version ?? "0.0.0",
|
|
53
|
+
description: meta.description ?? "",
|
|
54
|
+
path: extPath,
|
|
55
|
+
context: fs.existsSync(contextPath) ? contextPath : null,
|
|
56
|
+
commands: meta.commands ?? {},
|
|
57
|
+
raw: meta
|
|
58
|
+
});
|
|
59
|
+
} catch { /* skip malformed */ }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return extensions;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Collect extra context dirs from extensions for memory.js */
|
|
66
|
+
export function getExtensionContextDirs(extensions) {
|
|
67
|
+
return extensions.map(e => e.path);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Build all custom commands from: extensions + ~/.gemini/commands/** */
|
|
71
|
+
export async function loadCustomCommands(extensions) {
|
|
72
|
+
const cmds = {}; // "namespace:name" → { description, prompt, source }
|
|
73
|
+
|
|
74
|
+
// 1. From extensions
|
|
75
|
+
for (const ext of extensions) {
|
|
76
|
+
for (const [cmdName, cmd] of Object.entries(ext.commands)) {
|
|
77
|
+
const key = `${ext.name}:${cmdName}`;
|
|
78
|
+
cmds[key] = { ...cmd, source: `extension:${ext.name}` };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2. From ~/.gemini/commands/<namespace>/<name>.toml
|
|
83
|
+
if (fs.existsSync(CMD_DIR)) {
|
|
84
|
+
for (const ns of fs.readdirSync(CMD_DIR)) {
|
|
85
|
+
const nsDir = path.join(CMD_DIR, ns);
|
|
86
|
+
if (!fs.statSync(nsDir).isDirectory()) continue;
|
|
87
|
+
for (const file of fs.readdirSync(nsDir)) {
|
|
88
|
+
if (!file.endsWith(".toml")) continue;
|
|
89
|
+
try {
|
|
90
|
+
const { default: TOML } = await importToml();
|
|
91
|
+
const raw = fs.readFileSync(path.join(nsDir, file), "utf8");
|
|
92
|
+
const parsed = TOML.parse(raw);
|
|
93
|
+
const cmdName = file.replace(".toml", "");
|
|
94
|
+
cmds[`${ns}:${cmdName}`] = { ...parsed, source: `commands/${ns}` };
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return cmds;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Simple TOML loader (lazy import) */
|
|
104
|
+
async function importToml() {
|
|
105
|
+
return import("toml");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─────────────────────────────────────────────────────────────────
|
|
109
|
+
// INSTALL / UNINSTALL / ENABLE / DISABLE / LIST
|
|
110
|
+
// ─────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Install extension from:
|
|
114
|
+
* - local path: /path/to/my-ext
|
|
115
|
+
* - git URL: https://github.com/user/repo
|
|
116
|
+
*/
|
|
117
|
+
export async function installExtension(source) {
|
|
118
|
+
fs.mkdirSync(EXT_DIR, { recursive: true });
|
|
119
|
+
|
|
120
|
+
if (source.startsWith("http://") || source.startsWith("https://") || source.startsWith("git@")) {
|
|
121
|
+
// Git clone
|
|
122
|
+
const repoName = source.split("/").pop().replace(/\.git$/, "");
|
|
123
|
+
const destDir = path.join(EXT_DIR, repoName);
|
|
124
|
+
if (fs.existsSync(destDir)) return { error: `Extension "${repoName}" already installed.` };
|
|
125
|
+
await execAsync(`git clone --depth 1 "${source}" "${destDir}"`);
|
|
126
|
+
const manifest = path.join(destDir, "gemini-extension.json");
|
|
127
|
+
if (!fs.existsSync(manifest)) {
|
|
128
|
+
// Auto-create minimal manifest
|
|
129
|
+
fs.writeFileSync(manifest, JSON.stringify({ name: repoName, version: "0.0.0", enabled: true }, null, 2));
|
|
130
|
+
} else {
|
|
131
|
+
// Ensure enabled
|
|
132
|
+
const meta = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
133
|
+
meta.enabled = true;
|
|
134
|
+
fs.writeFileSync(manifest, JSON.stringify(meta, null, 2));
|
|
135
|
+
}
|
|
136
|
+
return { ok: true, name: repoName, path: destDir };
|
|
137
|
+
|
|
138
|
+
} else {
|
|
139
|
+
// Local path — create symlink or copy
|
|
140
|
+
const src = path.resolve(source);
|
|
141
|
+
if (!fs.existsSync(src)) return { error: `Path not found: ${src}` };
|
|
142
|
+
const manifest = path.join(src, "gemini-extension.json");
|
|
143
|
+
if (!fs.existsSync(manifest)) return { error: `No gemini-extension.json found in: ${src}` };
|
|
144
|
+
const meta = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
145
|
+
const name = meta.name ?? path.basename(src);
|
|
146
|
+
const dest = path.join(EXT_DIR, name);
|
|
147
|
+
if (fs.existsSync(dest)) return { error: `Extension "${name}" already installed.` };
|
|
148
|
+
// Copy (not symlink, for portability)
|
|
149
|
+
await execAsync(`cp -r "${src}" "${dest}"`);
|
|
150
|
+
meta.enabled = true;
|
|
151
|
+
fs.writeFileSync(path.join(dest, "gemini-extension.json"), JSON.stringify(meta, null, 2));
|
|
152
|
+
return { ok: true, name, path: dest };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function uninstallExtension(name) {
|
|
157
|
+
const dir = path.join(EXT_DIR, name);
|
|
158
|
+
if (!fs.existsSync(dir)) return { error: `Extension "${name}" not found.` };
|
|
159
|
+
await execAsync(`rm -rf "${dir}"`);
|
|
160
|
+
return { ok: true };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function enableExtension(name, enable = true) {
|
|
164
|
+
const manifest = path.join(EXT_DIR, name, "gemini-extension.json");
|
|
165
|
+
if (!fs.existsSync(manifest)) return { error: `Extension "${name}" not found.` };
|
|
166
|
+
const meta = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
167
|
+
meta.enabled = enable;
|
|
168
|
+
fs.writeFileSync(manifest, JSON.stringify(meta, null, 2));
|
|
169
|
+
return { ok: true };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function listExtensions() {
|
|
173
|
+
fs.mkdirSync(EXT_DIR, { recursive: true });
|
|
174
|
+
const results = [];
|
|
175
|
+
for (const name of fs.readdirSync(EXT_DIR)) {
|
|
176
|
+
const manifest = path.join(EXT_DIR, name, "gemini-extension.json");
|
|
177
|
+
if (!fs.existsSync(manifest)) continue;
|
|
178
|
+
try {
|
|
179
|
+
const meta = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
180
|
+
results.push({ name: meta.name ?? name, version: meta.version ?? "?", enabled: meta.enabled !== false, description: meta.description ?? "" });
|
|
181
|
+
} catch {}
|
|
182
|
+
}
|
|
183
|
+
return results;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function updateExtension(name) {
|
|
187
|
+
const dir = path.join(EXT_DIR, name);
|
|
188
|
+
if (!fs.existsSync(dir)) return { error: `Extension "${name}" not found.` };
|
|
189
|
+
try {
|
|
190
|
+
await execAsync(`git -C "${dir}" pull`);
|
|
191
|
+
return { ok: true };
|
|
192
|
+
} catch (e) {
|
|
193
|
+
return { error: e.message };
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/gemini.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// src/gemini.js — Gemini API client with native function calling
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import { fileTypeFromBuffer } from "file-type";
|
|
4
|
+
|
|
5
|
+
const CONFIG = {
|
|
6
|
+
URL: "https://us-central1-gemmy-ai-bdc03.cloudfunctions.net/gemini",
|
|
7
|
+
MODEL: "gemini-pro-latest",
|
|
8
|
+
HEADERS: {
|
|
9
|
+
"User-Agent": "okhttp/5.3.2",
|
|
10
|
+
"Accept-Encoding": "gzip",
|
|
11
|
+
"content-type": "application/json; charset=UTF-8"
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const SUPPORTED_MIMES = new Set([
|
|
16
|
+
"image/jpeg","image/png","image/gif","image/webp","image/heic","image/heif",
|
|
17
|
+
"video/mp4","video/mpeg","video/mov","video/avi","video/x-flv","video/mpg",
|
|
18
|
+
"video/webm","video/wmv","video/3gpp",
|
|
19
|
+
"audio/wav","audio/mp3","audio/aiff","audio/aac","audio/ogg","audio/flac",
|
|
20
|
+
"audio/mpeg","audio/ogg; codecs=opus",
|
|
21
|
+
"application/pdf","text/plain","text/html","text/css","text/javascript",
|
|
22
|
+
"text/x-typescript","text/csv","text/markdown","text/x-python",
|
|
23
|
+
"application/json","application/xml","application/rtf"
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
async function getToken() {
|
|
27
|
+
const { data } = await axios.post(
|
|
28
|
+
"https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key=AIzaSyAxof8_SbpDcww38NEQRhNh0Pzvbphh-IQ",
|
|
29
|
+
{ clientType: "CLIENT_TYPE_ANDROID" },
|
|
30
|
+
{ headers: {
|
|
31
|
+
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 12; SM-S9280 Build/AP3A.240905.015.A2)",
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
"X-Android-Package":"com.jetkite.gemmy",
|
|
34
|
+
"X-Android-Cert": "037CD2976D308B4EFD63EC63C48DC6E7AB7E5AF2",
|
|
35
|
+
"X-Firebase-GMPID": "1:652803432695:android:c4341db6033e62814f33f2"
|
|
36
|
+
}}
|
|
37
|
+
);
|
|
38
|
+
return data.idToken;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Core API call — supports:
|
|
43
|
+
* - plain chat (messages only)
|
|
44
|
+
* - file attachment (fileBuffer)
|
|
45
|
+
* - native function calling (tools = [{ functionDeclarations: [...] }])
|
|
46
|
+
* - function responses in history (parts with functionCall / functionResponse)
|
|
47
|
+
*/
|
|
48
|
+
export async function callGemini({ messages = [], fileBuffer = null, tools = null, systemInstruction = null } = {}) {
|
|
49
|
+
const token = await getToken();
|
|
50
|
+
|
|
51
|
+
// Separate system message
|
|
52
|
+
const sysMsg = messages.find(m => m.role === "system");
|
|
53
|
+
const sysText = systemInstruction
|
|
54
|
+
?? (sysMsg ? (typeof sysMsg.content === "string" ? sysMsg.content : sysMsg.parts?.[0]?.text ?? "") : null);
|
|
55
|
+
|
|
56
|
+
// Build contents array — support both {role,content} and {role,parts} formats
|
|
57
|
+
const contents = messages
|
|
58
|
+
.filter(m => m.role !== "system")
|
|
59
|
+
.map(m => {
|
|
60
|
+
if (m.parts) {
|
|
61
|
+
// Already in Gemini parts format (used for functionCall / functionResponse turns)
|
|
62
|
+
return { role: m.role === "assistant" ? "model" : m.role, parts: m.parts };
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
role: m.role === "assistant" ? "model" : m.role,
|
|
66
|
+
parts: [{ text: typeof m.content === "string" ? m.content : JSON.stringify(m.content) }]
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Attach file to last user message if provided
|
|
71
|
+
if (fileBuffer) {
|
|
72
|
+
const result = await fileTypeFromBuffer(fileBuffer);
|
|
73
|
+
const mimeType = result?.mime ?? "application/octet-stream";
|
|
74
|
+
if (!SUPPORTED_MIMES.has(mimeType)) throw new Error(`Unsupported file type: ${mimeType}`);
|
|
75
|
+
const filePart = { inlineData: { mimeType, data: fileBuffer.toString("base64") } };
|
|
76
|
+
const last = contents[contents.length - 1];
|
|
77
|
+
if (last?.role === "user") last.parts.push(filePart);
|
|
78
|
+
else contents.push({ role: "user", parts: [filePart] });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const payload = {
|
|
82
|
+
model: CONFIG.MODEL,
|
|
83
|
+
request: {
|
|
84
|
+
contents,
|
|
85
|
+
generationConfig: {
|
|
86
|
+
thinkingConfig: { thinkingLevel: "HIGH" },
|
|
87
|
+
temperature: 0
|
|
88
|
+
},
|
|
89
|
+
safetySettings: [
|
|
90
|
+
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" },
|
|
91
|
+
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" },
|
|
92
|
+
{ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" },
|
|
93
|
+
{ category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" }
|
|
94
|
+
],
|
|
95
|
+
// ── Native function calling ──────────────────────────────────────
|
|
96
|
+
...(tools?.length && { tools }),
|
|
97
|
+
// ── System instruction ───────────────────────────────────────────
|
|
98
|
+
...(sysText && { systemInstruction: { role: "user", parts: [{ text: sysText }] } })
|
|
99
|
+
},
|
|
100
|
+
stream: false
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const { data } = await axios.post(CONFIG.URL, payload, {
|
|
104
|
+
headers: { ...CONFIG.HEADERS, authorization: `Bearer ${token}` }
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!data?.candidates?.length) throw new Error("No response candidates from API");
|
|
108
|
+
|
|
109
|
+
const candidate = data.candidates[0];
|
|
110
|
+
const parts = candidate.content?.parts ?? [];
|
|
111
|
+
|
|
112
|
+
return { parts, candidate, raw: data };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Convenience: plain text chat (no tools) */
|
|
116
|
+
export async function chat(messages = [], fileBuffer = null) {
|
|
117
|
+
const { parts } = await callGemini({ messages, fileBuffer });
|
|
118
|
+
return parts.map(p => p.text ?? "").join("");
|
|
119
|
+
}
|
package/src/memory.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// src/memory.js — GEMINI.md hierarchical context loader (mirrors Gemini CLI)
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
|
|
6
|
+
export const CONTEXT_FILENAME = "GEMINI.md";
|
|
7
|
+
export const GLOBAL_DIR = path.join(os.homedir(), ".gemini");
|
|
8
|
+
export const GLOBAL_CONTEXT = path.join(GLOBAL_DIR, CONTEXT_FILENAME);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Load all GEMINI.md files in order (same as Gemini CLI):
|
|
12
|
+
* 1. ~/.gemini/GEMINI.md — global user instructions
|
|
13
|
+
* 2. Extensions' GEMINI.md — loaded separately by extensions.js
|
|
14
|
+
* 3. Project root → CWD — walk up from CWD to .git root
|
|
15
|
+
*
|
|
16
|
+
* Supports @./path/to/other.md imports (recursive).
|
|
17
|
+
*/
|
|
18
|
+
export function loadMemory(extraDirs = []) {
|
|
19
|
+
const loaded = []; // { file, content }
|
|
20
|
+
|
|
21
|
+
// 1. Global
|
|
22
|
+
if (fs.existsSync(GLOBAL_CONTEXT)) {
|
|
23
|
+
loaded.push({ file: GLOBAL_CONTEXT, content: readWithImports(GLOBAL_CONTEXT) });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 2. Extra dirs (extensions inject their GEMINI.md here)
|
|
27
|
+
for (const dir of extraDirs) {
|
|
28
|
+
const p = path.join(dir, CONTEXT_FILENAME);
|
|
29
|
+
if (fs.existsSync(p)) {
|
|
30
|
+
loaded.push({ file: p, content: readWithImports(p) });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 3. Walk from CWD up to git root (or filesystem root)
|
|
35
|
+
const chain = walkUpToRoot(process.cwd());
|
|
36
|
+
// Reverse so parent-most context loads first, CWD context last (highest priority)
|
|
37
|
+
for (const dir of chain.reverse()) {
|
|
38
|
+
const p = path.join(dir, CONTEXT_FILENAME);
|
|
39
|
+
if (fs.existsSync(p)) {
|
|
40
|
+
loaded.push({ file: p, content: readWithImports(p) });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return loaded;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Concatenate all loaded context into one string for the system prompt */
|
|
48
|
+
export function buildContextString(loaded) {
|
|
49
|
+
if (!loaded.length) return null;
|
|
50
|
+
return loaded
|
|
51
|
+
.map(({ file, content }) => `<!-- context: ${file} -->\n${content}`)
|
|
52
|
+
.join("\n\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Walk up from dir to git root (or fs root), return all dirs in path */
|
|
56
|
+
function walkUpToRoot(startDir) {
|
|
57
|
+
const dirs = [];
|
|
58
|
+
let current = startDir;
|
|
59
|
+
while (true) {
|
|
60
|
+
dirs.push(current);
|
|
61
|
+
const parent = path.dirname(current);
|
|
62
|
+
if (parent === current) break; // filesystem root
|
|
63
|
+
// Stop at git root
|
|
64
|
+
if (fs.existsSync(path.join(current, ".git"))) break;
|
|
65
|
+
current = parent;
|
|
66
|
+
}
|
|
67
|
+
return dirs;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Read file and resolve @./import.md directives recursively */
|
|
71
|
+
export function readWithImports(filePath, depth = 0) {
|
|
72
|
+
if (depth > 10 || !fs.existsSync(filePath)) return "";
|
|
73
|
+
const src = fs.readFileSync(filePath, "utf8");
|
|
74
|
+
return src.replace(/^@(\.\/[^\s]+\.md)$/gm, (_, importPath) => {
|
|
75
|
+
const resolved = path.resolve(path.dirname(filePath), importPath);
|
|
76
|
+
return readWithImports(resolved, depth + 1);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────
|
|
81
|
+
// /memory commands
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
export function memoryShow(loaded) {
|
|
85
|
+
if (!loaded.length) return "(no GEMINI.md files loaded)";
|
|
86
|
+
return loaded.map(({ file, content }) =>
|
|
87
|
+
`\n${"═".repeat(60)}\n📄 ${file}\n${"═".repeat(60)}\n${content}`
|
|
88
|
+
).join("\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function memoryAdd(text) {
|
|
92
|
+
fs.mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
93
|
+
const existing = fs.existsSync(GLOBAL_CONTEXT) ? fs.readFileSync(GLOBAL_CONTEXT, "utf8") : "";
|
|
94
|
+
const sep = existing.endsWith("\n") ? "" : "\n";
|
|
95
|
+
fs.appendFileSync(GLOBAL_CONTEXT, `${sep}\n${text}\n`, "utf8");
|
|
96
|
+
return `Appended to ${GLOBAL_CONTEXT}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function ensureGlobalDir() {
|
|
100
|
+
fs.mkdirSync(path.join(GLOBAL_DIR, "extensions"), { recursive: true });
|
|
101
|
+
fs.mkdirSync(path.join(GLOBAL_DIR, "commands"), { recursive: true });
|
|
102
|
+
}
|
package/src/renderer.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// src/renderer.js — Terminal rendering (markdown, syntax, boxes, UI)
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────
|
|
5
|
+
// Syntax highlighting
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────
|
|
7
|
+
const KW = {
|
|
8
|
+
js: /\b(const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|new|this|class|extends|import|export|default|async|await|try|catch|finally|throw|typeof|instanceof|of|in|null|undefined|true|false|void|delete|yield|from|static|super|get|set)\b/g,
|
|
9
|
+
ts: /\b(const|let|var|function|return|if|else|for|while|switch|case|class|extends|import|export|default|async|await|try|catch|type|interface|enum|implements|declare|readonly|abstract|as|keyof|never|any|string|number|boolean|null|undefined|true|false)\b/g,
|
|
10
|
+
py: /\b(def|class|return|if|elif|else|for|while|import|from|as|with|try|except|finally|raise|pass|break|continue|and|or|not|in|is|None|True|False|lambda|yield|global|async|await)\b/g,
|
|
11
|
+
go: /\b(func|return|if|else|for|range|switch|var|const|type|struct|interface|import|package|defer|go|chan|map|make|new|nil|true|false)\b/g,
|
|
12
|
+
sh: /\b(if|then|else|elif|fi|for|while|do|done|case|esac|function|return|exit|export|echo|local|read|source|alias)\b/g,
|
|
13
|
+
rs: /\b(fn|let|mut|return|if|else|for|match|use|mod|pub|struct|enum|impl|trait|type|const|static|async|await|true|false|None|Some|Ok|Err)\b/g,
|
|
14
|
+
};
|
|
15
|
+
const LANGMAP = { javascript:"js",js:"js",typescript:"ts",ts:"ts",python:"py",py:"py",go:"go",golang:"go",rust:"rs",rs:"rs",bash:"sh",sh:"sh",shell:"sh",zsh:"sh" };
|
|
16
|
+
|
|
17
|
+
function highlight(code, lang = "") {
|
|
18
|
+
const l = LANGMAP[lang.toLowerCase()] || "";
|
|
19
|
+
let r = code;
|
|
20
|
+
const saved = [];
|
|
21
|
+
const save = s => { const id = `\x00${saved.length}\x00`; saved.push(s); return id; };
|
|
22
|
+
|
|
23
|
+
// Comments
|
|
24
|
+
r = r.replace(/(\/\/.*$|#.*$|\/\*[\s\S]*?\*\/)/gm, m => save(chalk.hex("#6A9955").italic(m)));
|
|
25
|
+
// Strings
|
|
26
|
+
r = r.replace(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, m => save(chalk.hex("#CE9178")(m)));
|
|
27
|
+
// Keywords
|
|
28
|
+
if (KW[l]) r = r.replace(KW[l], m => save(chalk.hex("#569CD6")(m)));
|
|
29
|
+
// Numbers
|
|
30
|
+
r = r.replace(/\b(\d+\.?\d*)\b/g, m => save(chalk.hex("#B5CEA8")(m)));
|
|
31
|
+
// Function calls
|
|
32
|
+
r = r.replace(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?=\()/g, m => save(chalk.hex("#DCDCAA")(m)));
|
|
33
|
+
// Restore
|
|
34
|
+
return r.replace(/\x00(\d+)\x00/g, (_, i) => saved[parseInt(i)]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────
|
|
38
|
+
// Markdown → terminal
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────
|
|
40
|
+
export function renderMarkdown(text) {
|
|
41
|
+
let r = text;
|
|
42
|
+
|
|
43
|
+
// Fenced code blocks
|
|
44
|
+
r = r.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
|
|
45
|
+
const hl = highlight(code.trim(), lang);
|
|
46
|
+
const lbl = lang ? chalk.hex("#858585")(` ${lang} `) : "";
|
|
47
|
+
const top = chalk.hex("#3C3C3C")("┌" + "─".repeat(58)) + lbl;
|
|
48
|
+
const bot = chalk.hex("#3C3C3C")("└" + "─".repeat(59));
|
|
49
|
+
const body = hl.split("\n").map(l => chalk.hex("#3C3C3C")("│ ") + l).join("\n");
|
|
50
|
+
return `\n${top}\n${body}\n${bot}\n`;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Inline code
|
|
54
|
+
r = r.replace(/`([^`\n]+)`/g, (_, c) => chalk.hex("#CE9178").bgHex("#2A2A2A")(` ${c} `));
|
|
55
|
+
|
|
56
|
+
// Headers
|
|
57
|
+
r = r.replace(/^### (.+)$/gm, (_, t) => chalk.hex("#DCDCAA").bold(` ◆ ${t}`));
|
|
58
|
+
r = r.replace(/^## (.+)$/gm, (_, t) => chalk.hex("#4EC9B0").bold(`\n ◈ ${t}`));
|
|
59
|
+
r = r.replace(/^# (.+)$/gm, (_, t) => chalk.hex("#569CD6").bold.underline(`\n ${t}`));
|
|
60
|
+
|
|
61
|
+
// Bold / italic
|
|
62
|
+
r = r.replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => chalk.bold.italic(t));
|
|
63
|
+
r = r.replace(/\*\*(.+?)\*\*/g, (_, t) => chalk.bold(t));
|
|
64
|
+
r = r.replace(/\*(.+?)\*/g, (_, t) => chalk.italic(t));
|
|
65
|
+
|
|
66
|
+
// Blockquotes
|
|
67
|
+
r = r.replace(/^> (.+)$/gm, (_, t) => chalk.hex("#6A9955")(` ▌ ${t}`));
|
|
68
|
+
|
|
69
|
+
// Lists
|
|
70
|
+
r = r.replace(/^(\s*)[*\-+] (.+)$/gm, (_, i, t) => `${i}${chalk.hex("#569CD6")("◆")} ${t}`);
|
|
71
|
+
r = r.replace(/^(\s*)(\d+)\. (.+)$/gm, (_, i, n, t) => `${i}${chalk.hex("#569CD6").bold(n+".")} ${t}`);
|
|
72
|
+
|
|
73
|
+
// HR
|
|
74
|
+
r = r.replace(/^---+$/gm, chalk.hex("#3C3C3C")("─".repeat(58)));
|
|
75
|
+
|
|
76
|
+
// Links
|
|
77
|
+
r = r.replace(/\[(.+?)\]\((.+?)\)/g, (_, txt, url) =>
|
|
78
|
+
chalk.bold(txt) + " " + chalk.hex("#858585").dim(`(${url})`)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return r;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─────────────────────────────────────────────────────────────────
|
|
85
|
+
// Box printers
|
|
86
|
+
// ─────────────────────────────────────────────────────────────────
|
|
87
|
+
const W = 60;
|
|
88
|
+
|
|
89
|
+
export function printUser(text) {
|
|
90
|
+
process.stdout.write(
|
|
91
|
+
"\n" +
|
|
92
|
+
chalk.hex("#569CD6").bold(" ╭─ You " + "─".repeat(W - 8)) + "\n" +
|
|
93
|
+
chalk.hex("#569CD6")(" │ ") + chalk.white(text) + "\n" +
|
|
94
|
+
chalk.hex("#569CD6")(" ╰" + "─".repeat(W)) + "\n"
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function printAssistant(text) {
|
|
99
|
+
process.stdout.write(
|
|
100
|
+
"\n" + chalk.hex("#4EC9B0").bold(" ╭─ Gemini " + "─".repeat(W - 10)) + "\n"
|
|
101
|
+
);
|
|
102
|
+
renderMarkdown(text).split("\n").forEach(line =>
|
|
103
|
+
process.stdout.write(chalk.hex("#4EC9B0")(" │ ") + line + "\n")
|
|
104
|
+
);
|
|
105
|
+
process.stdout.write(chalk.hex("#4EC9B0")(" ╰" + "─".repeat(W)) + "\n\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function printError(msg) {
|
|
109
|
+
process.stdout.write("\n" + chalk.hex("#F44747")(" ✖ ") + chalk.hex("#CE9178")(msg) + "\n\n");
|
|
110
|
+
}
|
|
111
|
+
export function printInfo(msg) {
|
|
112
|
+
process.stdout.write(chalk.hex("#858585")(" ℹ ") + chalk.hex("#6A9955")(msg) + "\n");
|
|
113
|
+
}
|
|
114
|
+
export function printSuccess(msg) {
|
|
115
|
+
process.stdout.write(chalk.hex("#4EC9B0")(" ✔ ") + chalk.hex("#4EC9B0")(msg) + "\n");
|
|
116
|
+
}
|
|
117
|
+
export function printWarning(msg) {
|
|
118
|
+
process.stdout.write(chalk.hex("#DCDCAA")(" ⚠ ") + chalk.hex("#DCDCAA")(msg) + "\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─────────────────────────────────────────────────────────────────
|
|
122
|
+
// Welcome screen
|
|
123
|
+
// ─────────────────────────────────────────────────────────────────
|
|
124
|
+
export function renderWelcome(memCount = 0, extCount = 0, cmdCount = 0) {
|
|
125
|
+
return [
|
|
126
|
+
"",
|
|
127
|
+
chalk.hex("#4EC9B0").bold(" ██████╗ ███████╗███╗ ███╗██╗███╗ ██╗██╗"),
|
|
128
|
+
chalk.hex("#4EC9B0").bold(" ██╔════╝ ██╔════╝████╗ ████║██║████╗ ██║██║"),
|
|
129
|
+
chalk.hex("#569CD6").bold(" ██║ ███╗█████╗ ██╔████╔██║██║██╔██╗ ██║██║"),
|
|
130
|
+
chalk.hex("#569CD6").bold(" ██║ ██║██╔══╝ ██║╚██╔╝██║██║██║╚██╗██║██║"),
|
|
131
|
+
chalk.hex("#DCDCAA").bold(" ╚██████╔╝███████╗██║ ╚═╝ ██║██║██║ ╚████║██║"),
|
|
132
|
+
chalk.hex("#858585") (" ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝"),
|
|
133
|
+
"",
|
|
134
|
+
chalk.hex("#858585")(" ╔═══════════════════════════════════════════════════════╗"),
|
|
135
|
+
chalk.hex("#858585")(" ║ ") + chalk.hex("#4EC9B0")(" AI Agent CLI") + chalk.hex("#858585")(" · ") + chalk.hex("#DCDCAA")("native function calling") + chalk.hex("#858585")(" · ") + chalk.hex("#C586C0")("Gemini Pro ") + chalk.hex("#858585")(" ║"),
|
|
136
|
+
chalk.hex("#858585")(" ╚═══════════════════════════════════════════════════════╝"),
|
|
137
|
+
"",
|
|
138
|
+
chalk.hex("#6A9955")(
|
|
139
|
+
` Context files: ${memCount} · Extensions: ${extCount} · Custom commands: ${cmdCount}`
|
|
140
|
+
),
|
|
141
|
+
chalk.hex("#858585")(` Type ${chalk.hex("#CE9178")("/help")} for all commands`),
|
|
142
|
+
"",
|
|
143
|
+
].join("\n");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─────────────────────────────────────────────────────────────────
|
|
147
|
+
// Help screen
|
|
148
|
+
// ─────────────────────────────────────────────────────────────────
|
|
149
|
+
export function renderHelp(customCommands = {}) {
|
|
150
|
+
const builtIn = [
|
|
151
|
+
["/agent", "Toggle AGENT (ReAct loop) ↔ CHAT mode"],
|
|
152
|
+
["/yolo", "Toggle auto-approve all tool actions"],
|
|
153
|
+
["/memory show", "Show all loaded GEMINI.md context files"],
|
|
154
|
+
["/memory reload", "Reload all GEMINI.md files from disk"],
|
|
155
|
+
["/memory add <text>", "Append text to ~/.gemini/GEMINI.md"],
|
|
156
|
+
["/ext list", "List installed extensions"],
|
|
157
|
+
["/ext install <src>", "Install extension (path or git URL)"],
|
|
158
|
+
["/ext uninstall <n>", "Uninstall extension by name"],
|
|
159
|
+
["/ext enable <name>", "Enable an extension"],
|
|
160
|
+
["/ext disable <name>", "Disable an extension"],
|
|
161
|
+
["/ext update <name>", "Pull latest from git source"],
|
|
162
|
+
["/file <path>", "Attach file to next message"],
|
|
163
|
+
["/system <text>", "Set session system instruction"],
|
|
164
|
+
["/history", "Show conversation turns"],
|
|
165
|
+
["/export <file>", "Export history to JSON"],
|
|
166
|
+
["/cd <path>", "Change working directory"],
|
|
167
|
+
["/cwd", "Show working directory"],
|
|
168
|
+
["/new /clear", "Reset conversation"],
|
|
169
|
+
["/model", "Show model & config info"],
|
|
170
|
+
["/help", "Show this help"],
|
|
171
|
+
["/exit /quit", "Exit Gemini"],
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
const lines = [
|
|
175
|
+
"",
|
|
176
|
+
chalk.hex("#569CD6").bold(" ┌─ BUILT-IN COMMANDS ─────────────────────────────────────┐"),
|
|
177
|
+
...builtIn.map(([cmd, desc]) =>
|
|
178
|
+
chalk.hex("#569CD6")(" │ ") +
|
|
179
|
+
chalk.hex("#CE9178").bold(cmd.padEnd(26)) +
|
|
180
|
+
chalk.hex("#858585")(desc.padEnd(36)) +
|
|
181
|
+
chalk.hex("#569CD6")("│")
|
|
182
|
+
),
|
|
183
|
+
chalk.hex("#569CD6")(" └────────────────────────────────────────────────────────┘"),
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
const cmds = Object.entries(customCommands);
|
|
187
|
+
if (cmds.length) {
|
|
188
|
+
lines.push("", chalk.hex("#C586C0").bold(" ┌─ CUSTOM COMMANDS ─────────────────────────────────────────┐"));
|
|
189
|
+
cmds.forEach(([key, cmd]) => {
|
|
190
|
+
lines.push(
|
|
191
|
+
chalk.hex("#C586C0")(" │ ") +
|
|
192
|
+
chalk.hex("#DCDCAA").bold(("/" + key).padEnd(28)) +
|
|
193
|
+
chalk.hex("#858585")((cmd.description ?? "").padEnd(34)) +
|
|
194
|
+
chalk.hex("#C586C0")("│")
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
lines.push(chalk.hex("#C586C0")(" └───────────────────────────────────────────────────────────┘"));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
lines.push("", chalk.hex("#6A9955")(" Tip: /yolo skips all confirmations · Ctrl+C to cancel · Ctrl+D to exit"), "");
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
}
|