@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.
@@ -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
+ }
@@ -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
+ }