@ikyyofc/gemini-cli 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,9 +1,4 @@
1
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
2
  import readline from "readline";
8
3
  import fs from "fs";
9
4
  import path from "path";
@@ -15,7 +10,7 @@ import {
15
10
  loadMemory, buildContextString, memoryShow, memoryAdd, ensureGlobalDir, GLOBAL_DIR
16
11
  } from "./src/memory.js";
17
12
  import {
18
- loadExtensions, loadCustomCommands, getExtensionContextDirs,
13
+ loadExtensions, getExtensionContextDirs,
19
14
  installExtension, uninstallExtension, enableExtension, listExtensions, updateExtension
20
15
  } from "./src/extensions.js";
21
16
  import {
@@ -23,340 +18,346 @@ import {
23
18
  printUser, printAssistant,
24
19
  printError, printInfo, printSuccess, printWarning
25
20
  } from "./src/renderer.js";
21
+ import {
22
+ enableBracketedPaste, disableBracketedPaste, setupPaste
23
+ } from "./src/input.js";
26
24
 
27
25
  // ─────────────────────────────────────────────────────────────────
28
26
  // Bootstrap
29
27
  // ─────────────────────────────────────────────────────────────────
30
28
  ensureGlobalDir();
31
29
 
32
- let extensions = loadExtensions();
33
- let customCommands = {}; // populated after extensions load
34
- let memoryLoaded = []; // { file, content }[]
35
- let memoryContext = null; // concatenated string
30
+ let extensions = loadExtensions();
31
+ let memoryLoaded = [];
32
+ let memoryContext = null;
36
33
 
37
34
  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);
35
+ extensions = loadExtensions();
36
+ memoryLoaded = loadMemory(getExtensionContextDirs(extensions));
37
+ memoryContext = buildContextString(memoryLoaded);
43
38
  }
44
39
  reloadAll();
45
40
 
46
41
  // ─────────────────────────────────────────────────────────────────
47
42
  // State
48
43
  // ─────────────────────────────────────────────────────────────────
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;
44
+ let history = [];
45
+ let sessionSystem = null;
46
+ let pendingFile = null;
47
+ let pendingPath = null;
48
+ let processing = false;
49
+ let agentMode = true;
50
+ let autoApprove = false;
51
+
52
+ function sysInstruction() {
53
+ return [memoryContext, sessionSystem].filter(Boolean).join("\n\n") || null;
54
+ }
55
+
56
+ // ─────────────────────────────────────────────────────────────────
57
+ // Readline — directly on process.stdin, NO transform stream
58
+ // ─────────────────────────────────────────────────────────────────
59
+ const rl = readline.createInterface({
60
+ input: process.stdin,
61
+ output: process.stdout,
62
+ terminal: true,
63
+ historySize: 200,
64
+ crlfDelay: Infinity,
65
+ });
66
+
67
+ // Attach paste interception (keypress-based, not Transform-based)
68
+ enableBracketedPaste();
69
+ setupPaste(rl);
70
+
71
+ // Clean up on any exit
72
+ function cleanup() {
73
+ disableBracketedPaste();
74
+ if (process.stdin.isTTY) {
75
+ try { process.stdin.setRawMode(false); } catch {}
76
+ }
61
77
  }
78
+ process.on("exit", cleanup);
79
+ process.on("SIGTERM", () => { cleanup(); process.exit(0); });
62
80
 
63
81
  // ─────────────────────────────────────────────────────────────────
64
- // Readline
82
+ // Prompt
65
83
  // ─────────────────────────────────────────────────────────────────
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();
84
+ function showPrompt() {
85
+ const mode = agentMode ? chalk.hex("#4EC9B0")("agent") : chalk.dim("chat");
86
+ const yolo = autoApprove ? chalk.red(" yolo") : "";
87
+ const file = pendingFile ? chalk.yellow(` +${path.basename(pendingPath)}`) : "";
88
+ const turns = history.length ? chalk.dim(` ${Math.ceil(history.length/2)}t`) : "";
89
+ const mem = memoryLoaded.length ? chalk.dim(` m${memoryLoaded.length}`) : "";
90
+
91
+ rl.setPrompt(
92
+ "\n " + chalk.bold("❯") + " " +
93
+ chalk.dim("[") + mode + yolo + file + turns + mem + chalk.dim("]") + " "
94
+ );
95
+ rl.prompt(true);
76
96
  }
77
97
 
78
98
  // ─────────────────────────────────────────────────────────────────
79
99
  // File attachment
80
100
  // ─────────────────────────────────────────────────────────────────
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.");
101
+ function attachFile(fp) {
102
+ const p = path.resolve(fp.trim().replace(/^['"]|['"]$/g, ""));
103
+ if (!fs.existsSync(p)) { printError(`not found: ${p}`); return; }
104
+ if (fs.statSync(p).size > 20*1024*1024) { printError("file too large (max 20MB)"); return; }
105
+ pendingFile = fs.readFileSync(p);
106
+ pendingPath = p;
107
+ printSuccess(`attached ${path.basename(p)}`);
108
+ }
109
+
110
+ // ─────────────────────────────────────────────────────────────────
111
+ // Send message
112
+ // ─────────────────────────────────────────────────────────────────
113
+ async function send(userText) {
114
+ if (!userText.trim()) return;
115
+
116
+ printUser(userText + (pendingFile ? chalk.dim(` [${path.basename(pendingPath)}]`) : ""));
117
+
118
+ if (agentMode) {
119
+ const res = await runAgentLoop(userText, history, {
120
+ systemInstruction: sysInstruction(),
121
+ autoApprove,
122
+ maxIterations: 40,
123
+ });
124
+ if (res?.finalResponse) {
125
+ history.push({ role: "user", content: userText });
126
+ history.push({ role: "assistant", content: res.finalResponse });
127
+ }
128
+ } else {
129
+ const { default: ora } = await import("ora");
130
+ const sp = ora({ text: "thinking…", spinner: "dots", color: "cyan", prefixText: " " }).start();
131
+ const msgs = [];
132
+ if (sysInstruction()) msgs.push({ role: "system", content: sysInstruction() });
133
+ msgs.push(...history, { role: "user", content: userText });
134
+ try {
135
+ const t0 = Date.now();
136
+ const reply = await chat(msgs, pendingFile || null);
137
+ sp.succeed(chalk.dim(`done (${((Date.now()-t0)/1000).toFixed(1)}s)`));
138
+ history.push({ role: "user", content: userText }, { role: "assistant", content: reply });
139
+ printAssistant(reply);
140
+ } catch (e) {
141
+ sp.fail("failed");
142
+ printError(e.message);
143
+ }
144
+ }
145
+
146
+ pendingFile = null;
147
+ pendingPath = null;
89
148
  }
90
149
 
91
150
  // ─────────────────────────────────────────────────────────────────
92
- // Command router
151
+ // Commands
93
152
  // ─────────────────────────────────────────────────────────────────
94
153
  async function handleCommand(input) {
95
154
  const tokens = input.trim().split(/\s+/);
96
155
  const cmd = tokens[0].toLowerCase();
97
156
  const arg = tokens.slice(1).join(" ").trim();
98
157
 
99
- // ── /memory ─────────────────────────────────────────────────
100
158
  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
- }
159
+ const sub = tokens[1];
160
+ if (sub === "show") { console.log(memoryShow(memoryLoaded)); }
161
+ else if (sub === "reload") { reloadAll(); printSuccess(`reloaded — ${memoryLoaded.length} file(s)`); }
162
+ else if (sub === "add") {
163
+ const t = tokens.slice(2).join(" ");
164
+ if (!t) { printError("usage: /memory add <text>"); return; }
165
+ printSuccess(memoryAdd(t)); reloadAll();
166
+ } else { printInfo("/memory [show | reload | add <text>]"); }
116
167
  return;
117
168
  }
118
169
 
119
- // ── /ext ────────────────────────────────────────────────────
120
170
  if (cmd === "/ext") {
121
- const sub = tokens[1]?.toLowerCase();
171
+ const sub = tokens[1];
122
172
  const name = tokens[2];
123
-
124
173
  if (sub === "list") {
125
174
  const list = listExtensions();
126
- if (!list.length) { printInfo("No extensions installed. Use /ext install <source>"); return; }
175
+ if (!list.length) { printInfo("no extensions installed"); return; }
127
176
  console.log("");
128
177
  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
- );
178
+ const s = e.enabled ? chalk.green("") : chalk.dim("○");
179
+ console.log(` ${s} ${chalk.bold(e.name)} ${chalk.dim("v"+e.version)} ${chalk.dim(e.description)}`);
136
180
  });
137
181
  console.log("");
138
-
139
182
  } else if (sub === "install") {
140
- if (!arg.slice(arg.indexOf(" ")+1).trim() && !name) { printError("Usage: /ext install <path-or-git-url>"); return; }
141
183
  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
-
184
+ if (!src) { printError("usage: /ext install <path-or-url>"); return; }
185
+ printInfo(`installing…`);
186
+ const r = await installExtension(src);
187
+ r.error ? printError(r.error) : (printSuccess(`installed: ${r.name}`), reloadAll());
147
188
  } 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
-
189
+ if (!name) { printError("usage: /ext uninstall <n>"); return; }
190
+ const r = await uninstallExtension(name);
191
+ r.error ? printError(r.error) : (printSuccess(`removed: ${name}`), reloadAll());
153
192
  } 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
-
193
+ if (!name) { printError("usage: /ext enable <n>"); return; }
194
+ const r = enableExtension(name, true);
195
+ r.error ? printError(r.error) : (printSuccess(`enabled: ${name}`), reloadAll());
158
196
  } 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
-
197
+ if (!name) { printError("usage: /ext disable <n>"); return; }
198
+ const r = enableExtension(name, false);
199
+ r.error ? printError(r.error) : (printSuccess(`disabled: ${name}`), reloadAll());
163
200
  } 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
- }
201
+ if (!name) { printError("usage: /ext update <n>"); return; }
202
+ printInfo(`updating ${name}…`);
203
+ const r = await updateExtension(name);
204
+ r.error ? printError(r.error) : (printSuccess(`updated: ${name}`), reloadAll());
205
+ } else { printInfo("/ext [list | install | uninstall | enable | disable | update]"); }
172
206
  return;
173
207
  }
174
208
 
175
- // ── Built-in commands ────────────────────────────────────────
176
209
  switch (cmd) {
177
- case "/help":
178
- console.log(renderHelp(customCommands));
179
- break;
210
+ case "/help": console.log(renderHelp()); break;
180
211
 
181
212
  case "/new": case "/clear":
182
- conversationHistory = [];
183
- pendingFile = null; pendingFilePath = null;
184
- printInfo("Conversation cleared.");
213
+ history = []; pendingFile = null; pendingPath = null;
214
+ printInfo("conversation cleared");
185
215
  break;
186
216
 
187
217
  case "/file":
188
- if (!arg) { printError("Usage: /file <path>"); break; }
189
- loadFile(arg);
190
- break;
218
+ if (!arg) { printError("usage: /file <path>"); break; }
219
+ attachFile(arg); break;
191
220
 
192
221
  case "/system":
193
- if (!arg) { printInfo(`System: ${sessionSystem ?? "(none)"}`); break; }
194
- sessionSystem = arg;
195
- printSuccess("Session system instruction set.");
196
- break;
222
+ if (!arg) { printInfo(`system: ${sessionSystem ?? "(none)"}`); break; }
223
+ sessionSystem = arg; printSuccess("system instruction set"); break;
197
224
 
198
225
  case "/agent":
199
226
  agentMode = !agentMode;
200
- printSuccess(`Mode: ${agentMode ? chalk.hex("#4EC9B0").bold("AGENT") + " (native function calling loop)" : chalk.hex("#858585").bold("CHAT") + " (plain conversation)"}`);
227
+ printInfo(`mode ${agentMode ? chalk.hex("#4EC9B0")("agent") : chalk.dim("chat")}`);
201
228
  break;
202
229
 
203
230
  case "/yolo":
204
231
  autoApprove = !autoApprove;
205
- autoApprove ? printWarning("YOLO ON — all tool actions auto-approved.") : printSuccess("YOLO OFF — confirmations restored.");
232
+ autoApprove
233
+ ? printWarning("yolo on — all tool actions auto-approved")
234
+ : printInfo("yolo off — confirmations restored");
206
235
  break;
207
236
 
208
237
  case "/history":
209
- if (!conversationHistory.length) { printInfo("No history."); break; }
238
+ if (!history.length) { printInfo("no history"); break; }
210
239
  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));
240
+ history.forEach((m, i) => {
241
+ const who = m.role === "user" ? chalk.hex("#4A9EFF")("you") : chalk.hex("#4EC9B0")("gemini");
242
+ const body = String(m.content).replace(/\n/g, " ").slice(0, 90);
243
+ console.log(` ${chalk.dim("["+( i+1)+"]")} ${who} ${chalk.dim(body)}`);
216
244
  });
217
245
  console.log("");
218
246
  break;
219
247
 
220
248
  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}`);
249
+ if (!arg) { printError("usage: /export <file.json>"); break; }
250
+ fs.writeFileSync(path.resolve(arg), JSON.stringify({
251
+ exported_at: new Date().toISOString(),
252
+ mode: agentMode ? "agent" : "chat",
253
+ system: sysInstruction(),
254
+ messages: history,
255
+ }, null, 2));
256
+ printSuccess(`exported → ${arg}`);
224
257
  break;
225
258
 
226
259
  case "/cwd":
227
- printInfo(`Working directory: ${process.cwd()}`);
228
- break;
260
+ printInfo(process.cwd()); break;
229
261
 
230
262
  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); }
263
+ if (!arg) { printError("usage: /cd <path>"); break; }
264
+ try { process.chdir(path.resolve(arg)); printInfo(process.cwd()); }
265
+ catch (e) { printError(e.message); }
233
266
  break;
234
267
 
235
268
  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}`);
269
+ printInfo(`model gemini-pro-latest`);
270
+ printInfo(`tools ${agentMode ? "on (native function calling)" : "off"}`);
271
+ printInfo(`yolo ${autoApprove ? "on" : "off"}`);
272
+ printInfo(`memory ${memoryLoaded.length} file(s) · extensions: ${extensions.length}`);
273
+ printInfo(`config ${GLOBAL_DIR}`);
244
274
  break;
245
275
 
246
276
  case "/exit": case "/quit":
247
- printInfo("Goodbye! 👋");
248
- rl.close(); process.exit(0);
277
+ cleanup();
278
+ printInfo("bye");
279
+ process.exit(0);
249
280
  break;
250
281
 
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
- }
282
+ default:
283
+ printError(`unknown command: ${cmd} — /help for list`);
263
284
  }
264
285
  }
265
286
 
266
287
  // ─────────────────────────────────────────────────────────────────
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
288
+ // Main REPL loop
308
289
  // ─────────────────────────────────────────────────────────────────
309
290
  async function main() {
310
291
  process.stdout.write("\x1Bc");
311
- console.log(renderWelcome(memoryLoaded.length, extensions.length, Object.keys(customCommands).length));
292
+ console.log(renderWelcome(memoryLoaded.length, extensions.length));
312
293
 
313
294
  // Parse flags
314
295
  const argv = process.argv.slice(2);
315
296
  const positional = [];
316
297
  for (let i = 0; i < argv.length; i++) {
317
298
  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; }
299
+ if (a === "--system" && argv[i+1]) { sessionSystem = argv[++i]; }
300
+ else if (a === "--file" && argv[i+1]) { attachFile(argv[++i]); }
301
+ else if (a === "--yolo") { autoApprove = true; printWarning("yolo on"); }
302
+ else if (a === "--chat") { agentMode = false; }
322
303
  else if (!a.startsWith("--")) { positional.push(a); }
323
304
  }
324
305
 
325
- // One-shot
306
+ // One-shot mode
326
307
  if (positional.length > 0) {
327
- await sendMessage(positional.join(" "));
308
+ await send(positional.join(" "));
309
+ cleanup();
328
310
  process.exit(0);
329
311
  }
330
312
 
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("");
313
+ showPrompt();
314
+
315
+ rl.on("line", async (input) => {
316
+ // If already processing, just show prompt again do NOT block or pause stream
317
+ if (processing) {
318
+ printWarning("still thinking…");
319
+ showPrompt();
320
+ return;
321
+ }
342
322
 
343
- setPrompt();
323
+ const text = input.trim();
324
+ if (!text) { showPrompt(); return; }
344
325
 
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();
326
+ processing = true;
350
327
  try {
351
- if (input.startsWith("/")) await handleCommand(input);
352
- else await sendMessage(input);
328
+ if (text.startsWith("/")) await handleCommand(text);
329
+ else await send(input); // preserve original (may have \n from paste)
330
+ } catch (e) {
331
+ printError(e.message);
353
332
  } finally {
354
- isProcessing = false; rl.resume(); setPrompt();
333
+ processing = false;
334
+ showPrompt();
355
335
  }
356
336
  });
357
337
 
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(); });
338
+ // 'close' fires on Ctrl+D exit gracefully
339
+ rl.on("close", () => {
340
+ cleanup();
341
+ console.log("");
342
+ process.exit(0);
343
+ });
344
+
345
+ // SIGINT (Ctrl+C) — cancel current operation or exit
346
+ rl.on("SIGINT", () => {
347
+ if (processing) {
348
+ process.stdout.write("\n");
349
+ printWarning("ctrl+c again or /exit to quit");
350
+ showPrompt();
351
+ return;
352
+ }
353
+ // Not processing — treat as exit intent
354
+ cleanup();
355
+ console.log("");
356
+ process.exit(0);
357
+ });
360
358
  }
361
359
 
362
- main().catch(err => { console.error(chalk.red("\n Fatal:"), err.message); process.exit(1); });
360
+ main().catch(e => {
361
+ console.error(chalk.red("fatal:"), e.message);
362
+ process.exit(1);
363
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikyyofc/gemini-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "AI CLI Agent powered by Gemini — your own terminal assistant",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/src/input.js ADDED
@@ -0,0 +1,60 @@
1
+ // src/input.js — Bracketed paste via keypress interception
2
+ //
3
+ // No Transform stream. We intercept keypress events directly:
4
+ // \x1b[200~ → enter paste mode, start buffering lines
5
+ // \x1b[201~ → end paste mode, emit full buffer as one line
6
+ //
7
+ import readline from "readline";
8
+
9
+ export function enableBracketedPaste() { process.stdout.write("\x1b[?2004h"); }
10
+ export function disableBracketedPaste() { process.stdout.write("\x1b[?2004l"); }
11
+
12
+ /**
13
+ * Attach bracketed paste interception to an existing readline interface.
14
+ * readline must be created BEFORE calling this.
15
+ */
16
+ export function setupPaste(rl) {
17
+ // Make stdin emit 'keypress' events (readline may have already done this)
18
+ readline.emitKeypressEvents(process.stdin, rl);
19
+
20
+ let pasting = false;
21
+ let pasteBuf = [];
22
+
23
+ // Watch for paste start/end escape sequences
24
+ const onKeypress = (_str, key) => {
25
+ if (!key) return;
26
+ const seq = key.sequence ?? "";
27
+
28
+ if (seq === "\x1b[200~") {
29
+ pasting = true;
30
+ pasteBuf = [];
31
+ return;
32
+ }
33
+
34
+ if (seq === "\x1b[201~") {
35
+ pasting = false;
36
+ const full = pasteBuf.join("\n");
37
+ pasteBuf = [];
38
+ // Fire the complete paste as one synthetic 'line' event
39
+ rl.emit("line", full);
40
+ }
41
+ };
42
+
43
+ process.stdin.on("keypress", onKeypress);
44
+
45
+ // Patch rl.emit to swallow 'line' events while pasting
46
+ // (readline fires one per \n inside the paste — we buffer them)
47
+ const _emit = rl.emit.bind(rl);
48
+ rl.emit = function (event, ...args) {
49
+ if (event === "line" && pasting) {
50
+ pasteBuf.push(args[0] ?? "");
51
+ return false;
52
+ }
53
+ return _emit(event, ...args);
54
+ };
55
+
56
+ return () => {
57
+ process.stdin.off("keypress", onKeypress);
58
+ rl.emit = _emit;
59
+ };
60
+ }
package/src/renderer.js CHANGED
@@ -1,18 +1,22 @@
1
- // src/renderer.js — Terminal rendering (markdown, syntax, boxes, UI)
1
+ // src/renderer.js — Clean, minimal terminal UI
2
2
  import chalk from "chalk";
3
3
 
4
4
  // ─────────────────────────────────────────────────────────────────
5
5
  // Syntax highlighting
6
6
  // ─────────────────────────────────────────────────────────────────
7
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,
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)\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|source)\b/g,
13
+ rs: /\b(fn|let|mut|return|if|else|for|match|use|mod|pub|struct|enum|impl|trait|type|const|async|await|true|false|None|Some|Ok|Err)\b/g,
14
+ };
15
+ const LANGMAP = {
16
+ javascript:"js",js:"js",typescript:"ts",ts:"ts",
17
+ python:"py",py:"py",go:"go",golang:"go",
18
+ rust:"rs",rs:"rs",bash:"sh",sh:"sh",shell:"sh",zsh:"sh"
14
19
  };
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
20
 
17
21
  function highlight(code, lang = "") {
18
22
  const l = LANGMAP[lang.toLowerCase()] || "";
@@ -20,17 +24,11 @@ function highlight(code, lang = "") {
20
24
  const saved = [];
21
25
  const save = s => { const id = `\x00${saved.length}\x00`; saved.push(s); return id; };
22
26
 
23
- // Comments
24
27
  r = r.replace(/(\/\/.*$|#.*$|\/\*[\s\S]*?\*\/)/gm, m => save(chalk.hex("#6A9955").italic(m)));
25
- // Strings
26
28
  r = r.replace(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, m => save(chalk.hex("#CE9178")(m)));
27
- // Keywords
28
29
  if (KW[l]) r = r.replace(KW[l], m => save(chalk.hex("#569CD6")(m)));
29
- // Numbers
30
30
  r = r.replace(/\b(\d+\.?\d*)\b/g, m => save(chalk.hex("#B5CEA8")(m)));
31
- // Function calls
32
31
  r = r.replace(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?=\()/g, m => save(chalk.hex("#DCDCAA")(m)));
33
- // Restore
34
32
  return r.replace(/\x00(\d+)\x00/g, (_, i) => saved[parseInt(i)]);
35
33
  }
36
34
 
@@ -42,21 +40,20 @@ export function renderMarkdown(text) {
42
40
 
43
41
  // Fenced code blocks
44
42
  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`;
43
+ const hl = highlight(code.trimEnd(), lang);
44
+ const lbl = lang ? chalk.dim(` ${lang} `) : "";
45
+ const top = chalk.dim("".repeat(40)) + lbl;
46
+ const body = hl.split("\n").map(l => " " + l).join("\n");
47
+ return `\n ${top}\n${body}\n ${chalk.dim("".repeat(40))}\n`;
51
48
  });
52
49
 
53
50
  // Inline code
54
- r = r.replace(/`([^`\n]+)`/g, (_, c) => chalk.hex("#CE9178").bgHex("#2A2A2A")(` ${c} `));
51
+ r = r.replace(/`([^`\n]+)`/g, (_, c) => chalk.hex("#CE9178")(`\`${c}\``));
55
52
 
56
53
  // 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}`));
54
+ r = r.replace(/^### (.+)$/gm, (_, t) => "\n" + chalk.bold(t));
55
+ r = r.replace(/^## (.+)$/gm, (_, t) => "\n" + chalk.bold.underline(t));
56
+ r = r.replace(/^# (.+)$/gm, (_, t) => "\n" + chalk.bold.underline(t));
60
57
 
61
58
  // Bold / italic
62
59
  r = r.replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => chalk.bold.italic(t));
@@ -64,139 +61,124 @@ export function renderMarkdown(text) {
64
61
  r = r.replace(/\*(.+?)\*/g, (_, t) => chalk.italic(t));
65
62
 
66
63
  // Blockquotes
67
- r = r.replace(/^> (.+)$/gm, (_, t) => chalk.hex("#6A9955")(` ▌ ${t}`));
64
+ r = r.replace(/^> (.+)$/gm, (_, t) => chalk.dim("") + chalk.italic(t));
68
65
 
69
66
  // 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}`);
67
+ r = r.replace(/^(\s*)[*\-+] (.+)$/gm, (_, i, t) => `${i} · ${t}`);
68
+ r = r.replace(/^(\s*)(\d+)\. (.+)$/gm, (_, i, n, t) => `${i} ${chalk.bold(n+".")} ${t}`);
72
69
 
73
70
  // 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
- );
71
+ r = r.replace(/^---+$/gm, chalk.dim("─".repeat(48)));
80
72
 
81
73
  return r;
82
74
  }
83
75
 
84
76
  // ─────────────────────────────────────────────────────────────────
85
- // Box printers
77
+ // Message printers (clean, no heavy boxing)
86
78
  // ─────────────────────────────────────────────────────────────────
87
- const W = 60;
88
-
89
79
  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
- );
80
+ // Show multi-line messages cleanly
81
+ const lines = text.split("\n");
82
+ const prefix = chalk.hex("#4A9EFF").bold(" you ");
83
+ if (lines.length === 1) {
84
+ process.stdout.write("\n" + prefix + chalk.white(text) + "\n");
85
+ } else {
86
+ process.stdout.write("\n" + prefix + chalk.white(lines[0]) + "\n");
87
+ lines.slice(1).forEach(l => process.stdout.write(" " + chalk.white(l) + "\n"));
88
+ }
96
89
  }
97
90
 
98
91
  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");
92
+ const label = chalk.hex("#4EC9B0").bold("gemini ");
93
+ const lines = renderMarkdown(text).split("\n");
94
+ process.stdout.write("\n");
95
+ lines.forEach((line, i) => {
96
+ const pre = i === 0 ? " " + label : " ";
97
+ process.stdout.write(pre + line + "\n");
98
+ });
99
+ process.stdout.write("\n");
106
100
  }
107
101
 
108
102
  export function printError(msg) {
109
- process.stdout.write("\n" + chalk.hex("#F44747")(" ") + chalk.hex("#CE9178")(msg) + "\n\n");
103
+ process.stdout.write(chalk.red(" error ") + chalk.dim(msg) + "\n");
110
104
  }
111
105
  export function printInfo(msg) {
112
- process.stdout.write(chalk.hex("#858585")(" ") + chalk.hex("#6A9955")(msg) + "\n");
106
+ process.stdout.write(chalk.dim(" info ") + msg + "\n");
113
107
  }
114
108
  export function printSuccess(msg) {
115
- process.stdout.write(chalk.hex("#4EC9B0")(" ") + chalk.hex("#4EC9B0")(msg) + "\n");
109
+ process.stdout.write(chalk.green(" ") + msg + "\n");
116
110
  }
117
111
  export function printWarning(msg) {
118
- process.stdout.write(chalk.hex("#DCDCAA")(" ") + chalk.hex("#DCDCAA")(msg) + "\n");
112
+ process.stdout.write(chalk.yellow(" warn ") + msg + "\n");
119
113
  }
120
114
 
121
115
  // ─────────────────────────────────────────────────────────────────
122
- // Welcome screen
116
+ // Welcome
123
117
  // ─────────────────────────────────────────────────────────────────
124
- export function renderWelcome(memCount = 0, extCount = 0, cmdCount = 0) {
118
+ export function renderWelcome(memCount = 0, extCount = 0) {
119
+ const stats = [
120
+ memCount ? `${memCount} context file${memCount > 1 ? "s" : ""}` : null,
121
+ extCount ? `${extCount} extension${extCount > 1 ? "s" : ""}` : null,
122
+ ].filter(Boolean).join(" · ");
123
+
125
124
  return [
126
125
  "",
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`),
126
+ chalk.hex("#4EC9B0").bold(" Gemini CLI") +
127
+ chalk.dim(" ─ AI Agent ─ native function calling"),
128
+ stats ? chalk.dim(" " + stats) : "",
129
+ chalk.dim(" /help for commands · paste code freely · /agent to toggle tools"),
142
130
  "",
143
131
  ].join("\n");
144
132
  }
145
133
 
146
134
  // ─────────────────────────────────────────────────────────────────
147
- // Help screen
135
+ // Help
148
136
  // ─────────────────────────────────────────────────────────────────
149
137
  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 = [
138
+ const section = (title, rows) => [
175
139
  "",
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")("│")
140
+ chalk.dim(" " + title),
141
+ chalk.dim(" " + "─".repeat(48)),
142
+ ...rows.map(([cmd, desc]) =>
143
+ " " + chalk.hex("#CE9178")(cmd.padEnd(26)) + chalk.dim(desc)
182
144
  ),
183
- chalk.hex("#569CD6")(" └────────────────────────────────────────────────────────┘"),
145
+ ].join("\n");
146
+
147
+ const lines = [
148
+ section("COMMANDS", [
149
+ ["/agent", "Toggle agent (tools) ↔ chat mode"],
150
+ ["/yolo", "Skip all tool confirmations"],
151
+ ["/memory show", "Show loaded GEMINI.md files"],
152
+ ["/memory reload", "Reload context from disk"],
153
+ ["/memory add <text>", "Append to ~/.gemini/GEMINI.md"],
154
+ ["/ext list", "List extensions"],
155
+ ["/ext install <src>", "Install extension (path or git URL)"],
156
+ ["/ext uninstall <n>", "Remove extension"],
157
+ ["/ext enable/disable <n>", "Toggle extension"],
158
+ ["/ext update <n>", "Pull latest (git)"],
159
+ ["/file <path>", "Attach file to next message"],
160
+ ["/system <text>", "Set system instruction"],
161
+ ["/history", "Show conversation turns"],
162
+ ["/export <file>", "Export history to JSON"],
163
+ ["/cd <path>", "Change working directory"],
164
+ ["/new /clear", "Reset conversation"],
165
+ ["/model", "Show model info"],
166
+ ["/exit /quit", "Exit"],
167
+ ]),
184
168
  ];
185
169
 
186
170
  const cmds = Object.entries(customCommands);
187
171
  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")(" └───────────────────────────────────────────────────────────┘"));
172
+ lines.push(section("EXTENSION COMMANDS",
173
+ cmds.map(([k, c]) => ["/" + k, c.description ?? ""])
174
+ ));
198
175
  }
199
176
 
200
- lines.push("", chalk.hex("#6A9955")(" Tip: /yolo skips all confirmations · Ctrl+C to cancel · Ctrl+D to exit"), "");
177
+ lines.push(
178
+ "",
179
+ chalk.dim(" Paste multi-line code freely — input is collected until you press Enter."),
180
+ chalk.dim(" Ctrl+C cancel · Ctrl+D exit"),
181
+ ""
182
+ );
201
183
  return lines.join("\n");
202
- }
184
+ }