@ikyyofc/gemini-cli 1.0.4 → 1.0.6

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
@@ -19,7 +19,8 @@ import {
19
19
  printError, printInfo, printSuccess, printWarning
20
20
  } from "./src/renderer.js";
21
21
  import {
22
- enableBracketedPaste, disableBracketedPaste, setupPaste
22
+ PasteTransform, restorePaste,
23
+ enableBracketedPaste, disableBracketedPaste
23
24
  } from "./src/input.js";
24
25
 
25
26
  // ─────────────────────────────────────────────────────────────────
@@ -54,34 +55,46 @@ function sysInstruction() {
54
55
  }
55
56
 
56
57
  // ─────────────────────────────────────────────────────────────────
57
- // Readline — directly on process.stdin, NO transform stream
58
+ // Readline — stdin PasteTransform readline
59
+ // { end: false } prevents Ctrl+D on stdin from auto-closing readline
58
60
  // ─────────────────────────────────────────────────────────────────
61
+ enableBracketedPaste();
62
+
63
+ const pasteStream = new PasteTransform();
64
+ process.stdin.pipe(pasteStream, { end: false });
65
+
66
+ // Handle Ctrl+D (stdin end) manually
67
+ process.stdin.once("end", () => {
68
+ cleanup();
69
+ process.exit(0);
70
+ });
71
+
59
72
  const rl = readline.createInterface({
60
- input: process.stdin,
73
+ input: pasteStream,
61
74
  output: process.stdout,
62
75
  terminal: true,
63
76
  historySize: 200,
64
77
  crlfDelay: Infinity,
65
78
  });
66
79
 
67
- // Attach paste interception (keypress-based, not Transform-based)
68
- enableBracketedPaste();
69
- setupPaste(rl);
70
-
71
- // Clean up on any exit
72
80
  function cleanup() {
73
81
  disableBracketedPaste();
74
- if (process.stdin.isTTY) {
75
- try { process.stdin.setRawMode(false); } catch {}
76
- }
82
+ try { if (process.stdin.isTTY) process.stdin.setRawMode(false); } catch {}
77
83
  }
78
- process.on("exit", cleanup);
84
+ process.on("exit", cleanup);
79
85
  process.on("SIGTERM", () => { cleanup(); process.exit(0); });
80
86
 
87
+ // When terminal is resized (or focus returns on some terminals),
88
+ // only redraw prompt if we are NOT currently processing
89
+ process.on("SIGWINCH", () => { if (!processing) showPrompt(); });
90
+
81
91
  // ─────────────────────────────────────────────────────────────────
82
92
  // Prompt
83
93
  // ─────────────────────────────────────────────────────────────────
84
94
  function showPrompt() {
95
+ // Never redraw prompt while a request is in flight
96
+ if (processing) return;
97
+
85
98
  const mode = agentMode ? chalk.hex("#4EC9B0")("agent") : chalk.dim("chat");
86
99
  const yolo = autoApprove ? chalk.red(" yolo") : "";
87
100
  const file = pendingFile ? chalk.yellow(` +${path.basename(pendingPath)}`) : "";
@@ -96,7 +109,7 @@ function showPrompt() {
96
109
  }
97
110
 
98
111
  // ─────────────────────────────────────────────────────────────────
99
- // File attachment
112
+ // Attach file
100
113
  // ─────────────────────────────────────────────────────────────────
101
114
  function attachFile(fp) {
102
115
  const p = path.resolve(fp.trim().replace(/^['"]|['"]$/g, ""));
@@ -110,8 +123,10 @@ function attachFile(fp) {
110
123
  // ─────────────────────────────────────────────────────────────────
111
124
  // Send message
112
125
  // ─────────────────────────────────────────────────────────────────
113
- async function send(userText) {
114
- if (!userText.trim()) return;
126
+ async function send(rawLine) {
127
+ // Decode \x00 → \n from paste encoding
128
+ const userText = restorePaste(rawLine).trim();
129
+ if (!userText) return;
115
130
 
116
131
  printUser(userText + (pendingFile ? chalk.dim(` [${path.basename(pendingPath)}]`) : ""));
117
132
 
@@ -126,19 +141,20 @@ async function send(userText) {
126
141
  history.push({ role: "assistant", content: res.finalResponse });
127
142
  }
128
143
  } else {
129
- const { default: ora } = await import("ora");
130
- const sp = ora({ text: "thinking…", spinner: "dots", color: "cyan", prefixText: " ", discardStdin: false }).start();
144
+ process.stdout.write(chalk.hex("#555566")(" ⋯ ") + chalk.dim("thinking…") + "\n");
131
145
  const msgs = [];
132
146
  if (sysInstruction()) msgs.push({ role: "system", content: sysInstruction() });
133
147
  msgs.push(...history, { role: "user", content: userText });
134
148
  try {
135
149
  const t0 = Date.now();
136
150
  const reply = await chat(msgs, pendingFile || null);
137
- sp.succeed(chalk.dim(`done (${((Date.now()-t0)/1000).toFixed(1)}s)`));
151
+ // clear "thinking…" line, print done
152
+ if (process.stdout.isTTY) process.stdout.write("\x1b[1A\x1b[2K");
153
+ process.stdout.write(chalk.hex("#555566")(" ⋯ ") + chalk.dim(`done (${((Date.now()-t0)/1000).toFixed(1)}s)`) + "\n");
138
154
  history.push({ role: "user", content: userText }, { role: "assistant", content: reply });
139
155
  printAssistant(reply);
140
156
  } catch (e) {
141
- sp.fail("failed");
157
+ if (process.stdout.isTTY) process.stdout.write("\x1b[1A\x1b[2K");
142
158
  printError(e.message);
143
159
  }
144
160
  }
@@ -182,7 +198,7 @@ async function handleCommand(input) {
182
198
  } else if (sub === "install") {
183
199
  const src = tokens.slice(2).join(" ");
184
200
  if (!src) { printError("usage: /ext install <path-or-url>"); return; }
185
- printInfo(`installing…`);
201
+ printInfo("installing…");
186
202
  const r = await installExtension(src);
187
203
  r.error ? printError(r.error) : (printSuccess(`installed: ${r.name}`), reloadAll());
188
204
  } else if (sub === "uninstall") {
@@ -240,7 +256,7 @@ async function handleCommand(input) {
240
256
  history.forEach((m, i) => {
241
257
  const who = m.role === "user" ? chalk.hex("#4A9EFF")("you") : chalk.hex("#4EC9B0")("gemini");
242
258
  const body = String(m.content).replace(/\n/g, " ").slice(0, 90);
243
- console.log(` ${chalk.dim("["+( i+1)+"]")} ${who} ${chalk.dim(body)}`);
259
+ console.log(` ${chalk.dim("["+(i+1)+"]")} ${who} ${chalk.dim(body)}`);
244
260
  });
245
261
  console.log("");
246
262
  break;
@@ -256,8 +272,7 @@ async function handleCommand(input) {
256
272
  printSuccess(`exported → ${arg}`);
257
273
  break;
258
274
 
259
- case "/cwd":
260
- printInfo(process.cwd()); break;
275
+ case "/cwd": printInfo(process.cwd()); break;
261
276
 
262
277
  case "/cd":
263
278
  if (!arg) { printError("usage: /cd <path>"); break; }
@@ -274,10 +289,7 @@ async function handleCommand(input) {
274
289
  break;
275
290
 
276
291
  case "/exit": case "/quit":
277
- cleanup();
278
- printInfo("bye");
279
- process.exit(0);
280
- break;
292
+ cleanup(); console.log(""); process.exit(0); break;
281
293
 
282
294
  default:
283
295
  printError(`unknown command: ${cmd} — /help for list`);
@@ -285,13 +297,12 @@ async function handleCommand(input) {
285
297
  }
286
298
 
287
299
  // ─────────────────────────────────────────────────────────────────
288
- // Main REPL loop
300
+ // Main
289
301
  // ─────────────────────────────────────────────────────────────────
290
302
  async function main() {
291
303
  process.stdout.write("\x1Bc");
292
304
  console.log(renderWelcome(memoryLoaded.length, extensions.length));
293
305
 
294
- // Parse flags
295
306
  const argv = process.argv.slice(2);
296
307
  const positional = [];
297
308
  for (let i = 0; i < argv.length; i++) {
@@ -303,30 +314,30 @@ async function main() {
303
314
  else if (!a.startsWith("--")) { positional.push(a); }
304
315
  }
305
316
 
306
- // One-shot mode
307
317
  if (positional.length > 0) {
308
318
  await send(positional.join(" "));
309
- cleanup();
310
- process.exit(0);
319
+ cleanup(); process.exit(0);
311
320
  }
312
321
 
313
322
  showPrompt();
314
323
 
315
- rl.on("line", async (input) => {
316
- // If already processing, just show prompt again — do NOT block or pause stream
324
+ rl.on("line", async (rawLine) => {
325
+ // Decode paste encoding before doing anything else
326
+ const text = restorePaste(rawLine).trim();
327
+
328
+ if (!text) { showPrompt(); return; }
329
+
330
+ // Don't queue — just warn and re-prompt
317
331
  if (processing) {
318
332
  printWarning("still thinking…");
319
333
  showPrompt();
320
334
  return;
321
335
  }
322
336
 
323
- const text = input.trim();
324
- if (!text) { showPrompt(); return; }
325
-
326
337
  processing = true;
327
338
  try {
328
339
  if (text.startsWith("/")) await handleCommand(text);
329
- else await send(input); // preserve original (may have \n from paste)
340
+ else await send(rawLine); // send rawLine; send() decodes internally
330
341
  } catch (e) {
331
342
  printError(e.message);
332
343
  } finally {
@@ -335,14 +346,9 @@ async function main() {
335
346
  }
336
347
  });
337
348
 
338
- // 'close' fires on Ctrl+D exit gracefully
339
- rl.on("close", () => {
340
- cleanup();
341
- console.log("");
342
- process.exit(0);
343
- });
349
+ // readline 'close' = Ctrl+D via our manual handler, or rl.close() call
350
+ rl.on("close", () => { cleanup(); console.log(""); process.exit(0); });
344
351
 
345
- // SIGINT (Ctrl+C) — cancel current operation or exit
346
352
  rl.on("SIGINT", () => {
347
353
  if (processing) {
348
354
  process.stdout.write("\n");
@@ -350,14 +356,8 @@ async function main() {
350
356
  showPrompt();
351
357
  return;
352
358
  }
353
- // Not processing — treat as exit intent
354
- cleanup();
355
- console.log("");
356
- process.exit(0);
359
+ cleanup(); console.log(""); process.exit(0);
357
360
  });
358
361
  }
359
362
 
360
- main().catch(e => {
361
- console.error(chalk.red("fatal:"), e.message);
362
- process.exit(1);
363
- });
363
+ main().catch(e => { console.error(chalk.red("fatal:"), e.message); process.exit(1); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikyyofc/gemini-cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "AI CLI Agent powered by Gemini — your own terminal assistant",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/src/agent.js CHANGED
@@ -1,116 +1,115 @@
1
- // src/agent.js — ReAct agent loop using NATIVE Gemini function calling
1
+ // src/agent.js — ReAct agent loop, native Gemini function calling
2
2
  import chalk from "chalk";
3
- import ora from "ora";
4
3
  import { callGemini } from "./gemini.js";
5
4
  import { GEMINI_TOOLS, executeTool } from "./tools.js";
6
5
  import { printAssistant, printError, printWarning, printToolCall, printToolResult } from "./renderer.js";
7
6
 
8
- /**
9
- * Run the agent loop until the model gives a final text answer (no more tool calls).
10
- *
11
- * Native function calling flow:
12
- * 1. Send contents + tools (functionDeclarations) to Gemini
13
- * 2. Response parts may contain: { functionCall: { name, args } }
14
- * 3. Execute the tool → get result
15
- * 4. Append model turn (with functionCall) + user turn (with functionResponse)
16
- * 5. Repeat until response has only text parts → done
17
- */
7
+ // ─────────────────────────────────────────────────────────────────
8
+ // Static progress line write once, no continuous redraw
9
+ // Safe when terminal loses focus (no ANSI overwrite loops)
10
+ // ─────────────────────────────────────────────────────────────────
11
+ function printStep(n, label) {
12
+ process.stdout.write(
13
+ chalk.hex("#555566")(" ⋯ ") +
14
+ chalk.dim(label) +
15
+ (n > 1 ? chalk.hex("#555566")(` (step ${n})`) : "") +
16
+ "\n"
17
+ );
18
+ }
19
+
20
+ function clearLastLine() {
21
+ // Move cursor up one line and clear it — only called right after printStep
22
+ // so we can replace "thinking…" with the actual tool section header
23
+ if (process.stdout.isTTY) {
24
+ process.stdout.write("\x1b[1A\x1b[2K");
25
+ }
26
+ }
27
+
18
28
  export async function runAgentLoop(userMessage, history, {
19
29
  systemInstruction = null,
20
30
  autoApprove = false,
21
31
  maxIterations = 40,
22
32
  } = {}) {
23
33
 
24
- // Build initial message list
25
34
  const messages = [
26
35
  ...history,
27
36
  { role: "user", content: userMessage }
28
37
  ];
29
38
 
30
- let iteration = 0;
39
+ let iteration = 0;
40
+ let sectionOpen = false;
31
41
 
32
42
  while (iteration < maxIterations) {
33
43
  iteration++;
34
44
 
35
- // ── Call Gemini API with tools ──────────────────────────
36
- const spinner = ora({
37
- text: chalk.dim(iteration === 1 ? "thinking…" : `step ${iteration}…`),
38
- spinner: "dots",
39
- color: "cyan",
40
- prefixText: " ",
41
- discardStdin: false, // prevent ora from pausing stdin after stop
42
- }).start();
45
+ // Print a static "thinking…" line written once, not looping
46
+ printStep(iteration, iteration === 1 ? "thinking…" : "thinking…");
43
47
 
44
48
  let parts;
45
49
  try {
46
50
  const res = await callGemini({ messages, tools: GEMINI_TOOLS, systemInstruction });
47
51
  parts = res.parts;
48
- spinner.stop();
49
- clearLine();
52
+ // Clear the "thinking…" line now that we have a response
53
+ clearLastLine();
50
54
  } catch (err) {
51
- spinner.fail(chalk.hex("#F44747")("API error"));
55
+ clearLastLine();
52
56
  printError(err.message);
53
57
  return null;
54
58
  }
55
59
 
56
- // ── Separate text parts from functionCall parts ─────────
57
- const textParts = parts.filter(p => p.text != null);
60
+ const textParts = parts.filter(p => p.text != null);
58
61
  const callParts = parts.filter(p => p.functionCall != null);
59
62
 
60
- // Print any accompanying text (model thinking aloud)
63
+ // Model thinking aloud alongside tool calls
61
64
  const textContent = textParts.map(p => p.text).join("").trim();
62
65
  if (textContent && callParts.length > 0) {
63
66
  process.stdout.write(
64
- chalk.dim("" + textContent.replace(/\n/g, "\n ")) + "\n"
67
+ chalk.hex("#555566")(" ┄ ") +
68
+ chalk.dim(textContent.split("\n")[0].slice(0, 80)) +
69
+ "\n"
65
70
  );
66
71
  }
67
72
 
68
- // ── No tool calls final answer ────────────────────────
73
+ // ── Final answer (no more tool calls) ─────────────────────
69
74
  if (callParts.length === 0) {
70
75
  const final = textParts.map(p => p.text).join("").trim();
71
- // Close the tool section if it was opened
72
- if (iteration > 1) {
76
+ if (sectionOpen) {
73
77
  process.stdout.write(
74
78
  chalk.hex("#4A9EFF")(" ╰") +
75
79
  chalk.hex("#555566")("─".repeat(47)) + "\n"
76
80
  );
81
+ sectionOpen = false;
77
82
  }
78
83
  if (final) printAssistant(final);
79
84
  return { finalResponse: final, iterations: iteration };
80
85
  }
81
86
 
82
- // ── Execute each tool call ───────────────────────────────
87
+ // ── Tool calls ─────────────────────────────────────────────
83
88
  messages.push({ role: "model", parts });
84
89
 
85
- // Print tool section header on first iteration
86
- if (iteration === 1) {
90
+ if (!sectionOpen) {
87
91
  process.stdout.write(
88
92
  "\n" + chalk.hex("#4A9EFF")(" ╭─ working") +
89
93
  chalk.hex("#555566")(" " + "─".repeat(36)) + "\n"
90
94
  );
95
+ sectionOpen = true;
91
96
  }
92
97
 
93
98
  const responseParts = [];
94
-
95
99
  for (const part of callParts) {
96
100
  const { name, args } = part.functionCall;
97
101
  printToolCall(name, args);
98
-
99
102
  const result = await executeTool(name, args ?? {}, { autoApprove });
100
103
  printToolResult(result);
101
-
102
- responseParts.push({
103
- functionResponse: { name, response: result }
104
- });
104
+ responseParts.push({ functionResponse: { name, response: result } });
105
105
  }
106
106
 
107
107
  messages.push({ role: "user", parts: responseParts });
108
108
  }
109
109
 
110
- printWarning(`Max iterations (${maxIterations}) reached.`);
110
+ if (sectionOpen) {
111
+ process.stdout.write(chalk.hex("#4A9EFF")(" ╰") + chalk.hex("#555566")("─".repeat(47)) + "\n");
112
+ }
113
+ printWarning(`max iterations (${maxIterations}) reached`);
111
114
  return { finalResponse: null, iterations: iteration };
112
115
  }
113
-
114
- function clearLine() {
115
- if (process.stdout.clearLine) { process.stdout.clearLine(0); process.stdout.cursorTo(0); }
116
- }
package/src/input.js CHANGED
@@ -1,60 +1,96 @@
1
- // src/input.js — Bracketed paste via keypress interception
1
+ // src/input.js — Bracketed paste via Transform stream (byte-level interception)
2
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
3
+ // Keypress-based approach is unreliable because keypress events for \x1b[200~
4
+ // may arrive AFTER readline has already fired 'line' events for the pasted lines.
5
+ // Byte-level interception via Transform is the only reliable approach.
6
6
  //
7
- import readline from "readline";
7
+ import { Transform } from "stream";
8
8
 
9
- export function enableBracketedPaste() { process.stdout.write("\x1b[?2004h"); }
10
- export function disableBracketedPaste() { process.stdout.write("\x1b[?2004l"); }
9
+ const PASTE_START = "\x1b[200~";
10
+ const PASTE_END = "\x1b[201~";
11
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
- }
12
+ export class PasteTransform extends Transform {
13
+ constructor() {
14
+ super();
15
+ this._buf = ""; // raw accumulator
16
+ this._pasting = false;
17
+ this._pasteBuf = ""; // paste content accumulator
18
+ }
33
19
 
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
- };
20
+ _transform(chunk, _enc, cb) {
21
+ this._buf += chunk.toString("utf8");
22
+
23
+ let output = "";
24
+
25
+ while (this._buf.length > 0) {
26
+
27
+ if (!this._pasting) {
28
+ const si = this._buf.indexOf(PASTE_START);
29
+
30
+ if (si === -1) {
31
+ // No paste start found — check for partial escape at tail
32
+ const esc = this._buf.lastIndexOf("\x1b");
33
+ if (esc !== -1 && (this._buf.length - esc) < PASTE_START.length) {
34
+ // Could be partial escape sequence — flush safe part, hold rest
35
+ output += this._buf.slice(0, esc);
36
+ this._buf = this._buf.slice(esc);
37
+ break;
38
+ }
39
+ // No escape at all, pass through
40
+ output += this._buf;
41
+ this._buf = "";
42
+ break;
43
+ }
44
+
45
+ // Found paste start
46
+ output += this._buf.slice(0, si); // pass through text before it
47
+ this._buf = this._buf.slice(si + PASTE_START.length);
48
+ this._pasting = true;
49
+ this._pasteBuf = "";
42
50
 
43
- process.stdin.on("keypress", onKeypress);
51
+ } else {
52
+ // Inside paste — look for end marker
53
+ const ei = this._buf.indexOf(PASTE_END);
44
54
 
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;
55
+ if (ei === -1) {
56
+ // End not here yet buffer everything and wait
57
+ this._pasteBuf += this._buf;
58
+ this._buf = "";
59
+ break;
60
+ }
61
+
62
+ // Found paste end
63
+ this._pasteBuf += this._buf.slice(0, ei);
64
+ this._buf = this._buf.slice(ei + PASTE_END.length);
65
+ this._pasting = false;
66
+
67
+ // Replace newlines with \x00 so readline treats whole paste as ONE line
68
+ const encoded = this._pasteBuf
69
+ .replace(/\r\n/g, "\x00")
70
+ .replace(/\r/g, "\x00")
71
+ .replace(/\n/g, "\x00");
72
+
73
+ output += encoded + "\n"; // \n makes readline fire one 'line' event
74
+ this._pasteBuf = "";
75
+ }
52
76
  }
53
- return _emit(event, ...args);
54
- };
55
77
 
56
- return () => {
57
- process.stdin.off("keypress", onKeypress);
58
- rl.emit = _emit;
59
- };
78
+ if (output) this.push(output);
79
+ cb();
80
+ }
81
+
82
+ _flush(cb) {
83
+ // Flush anything remaining
84
+ if (this._buf) this.push(this._buf);
85
+ if (this._pasteBuf) this.push(this._pasteBuf);
86
+ cb();
87
+ }
60
88
  }
89
+
90
+ /** Decode: \x00 → \n (undo the encoding done above) */
91
+ export function restorePaste(line) {
92
+ return line.replace(/\x00/g, "\n");
93
+ }
94
+
95
+ export function enableBracketedPaste() { process.stdout.write("\x1b[?2004h"); }
96
+ export function disableBracketedPaste() { process.stdout.write("\x1b[?2004l"); }