@ikyyofc/gemini-cli 1.0.4 → 1.0.5

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.
Files changed (3) hide show
  1. package/index.js +44 -49
  2. package/package.json +1 -1
  3. package/src/input.js +86 -50
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,26 +55,31 @@ 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
84
  process.on("exit", cleanup);
79
85
  process.on("SIGTERM", () => { cleanup(); process.exit(0); });
@@ -96,7 +102,7 @@ function showPrompt() {
96
102
  }
97
103
 
98
104
  // ─────────────────────────────────────────────────────────────────
99
- // File attachment
105
+ // Attach file
100
106
  // ─────────────────────────────────────────────────────────────────
101
107
  function attachFile(fp) {
102
108
  const p = path.resolve(fp.trim().replace(/^['"]|['"]$/g, ""));
@@ -110,8 +116,10 @@ function attachFile(fp) {
110
116
  // ─────────────────────────────────────────────────────────────────
111
117
  // Send message
112
118
  // ─────────────────────────────────────────────────────────────────
113
- async function send(userText) {
114
- if (!userText.trim()) return;
119
+ async function send(rawLine) {
120
+ // Decode \x00 → \n from paste encoding
121
+ const userText = restorePaste(rawLine).trim();
122
+ if (!userText) return;
115
123
 
116
124
  printUser(userText + (pendingFile ? chalk.dim(` [${path.basename(pendingPath)}]`) : ""));
117
125
 
@@ -127,7 +135,10 @@ async function send(userText) {
127
135
  }
128
136
  } else {
129
137
  const { default: ora } = await import("ora");
130
- const sp = ora({ text: "thinking…", spinner: "dots", color: "cyan", prefixText: " ", discardStdin: false }).start();
138
+ const sp = ora({
139
+ text: "thinking…", spinner: "dots", color: "cyan",
140
+ prefixText: " ", discardStdin: false
141
+ }).start();
131
142
  const msgs = [];
132
143
  if (sysInstruction()) msgs.push({ role: "system", content: sysInstruction() });
133
144
  msgs.push(...history, { role: "user", content: userText });
@@ -182,7 +193,7 @@ async function handleCommand(input) {
182
193
  } else if (sub === "install") {
183
194
  const src = tokens.slice(2).join(" ");
184
195
  if (!src) { printError("usage: /ext install <path-or-url>"); return; }
185
- printInfo(`installing…`);
196
+ printInfo("installing…");
186
197
  const r = await installExtension(src);
187
198
  r.error ? printError(r.error) : (printSuccess(`installed: ${r.name}`), reloadAll());
188
199
  } else if (sub === "uninstall") {
@@ -240,7 +251,7 @@ async function handleCommand(input) {
240
251
  history.forEach((m, i) => {
241
252
  const who = m.role === "user" ? chalk.hex("#4A9EFF")("you") : chalk.hex("#4EC9B0")("gemini");
242
253
  const body = String(m.content).replace(/\n/g, " ").slice(0, 90);
243
- console.log(` ${chalk.dim("["+( i+1)+"]")} ${who} ${chalk.dim(body)}`);
254
+ console.log(` ${chalk.dim("["+(i+1)+"]")} ${who} ${chalk.dim(body)}`);
244
255
  });
245
256
  console.log("");
246
257
  break;
@@ -256,8 +267,7 @@ async function handleCommand(input) {
256
267
  printSuccess(`exported → ${arg}`);
257
268
  break;
258
269
 
259
- case "/cwd":
260
- printInfo(process.cwd()); break;
270
+ case "/cwd": printInfo(process.cwd()); break;
261
271
 
262
272
  case "/cd":
263
273
  if (!arg) { printError("usage: /cd <path>"); break; }
@@ -274,10 +284,7 @@ async function handleCommand(input) {
274
284
  break;
275
285
 
276
286
  case "/exit": case "/quit":
277
- cleanup();
278
- printInfo("bye");
279
- process.exit(0);
280
- break;
287
+ cleanup(); console.log(""); process.exit(0); break;
281
288
 
282
289
  default:
283
290
  printError(`unknown command: ${cmd} — /help for list`);
@@ -285,13 +292,12 @@ async function handleCommand(input) {
285
292
  }
286
293
 
287
294
  // ─────────────────────────────────────────────────────────────────
288
- // Main REPL loop
295
+ // Main
289
296
  // ─────────────────────────────────────────────────────────────────
290
297
  async function main() {
291
298
  process.stdout.write("\x1Bc");
292
299
  console.log(renderWelcome(memoryLoaded.length, extensions.length));
293
300
 
294
- // Parse flags
295
301
  const argv = process.argv.slice(2);
296
302
  const positional = [];
297
303
  for (let i = 0; i < argv.length; i++) {
@@ -303,30 +309,30 @@ async function main() {
303
309
  else if (!a.startsWith("--")) { positional.push(a); }
304
310
  }
305
311
 
306
- // One-shot mode
307
312
  if (positional.length > 0) {
308
313
  await send(positional.join(" "));
309
- cleanup();
310
- process.exit(0);
314
+ cleanup(); process.exit(0);
311
315
  }
312
316
 
313
317
  showPrompt();
314
318
 
315
- rl.on("line", async (input) => {
316
- // If already processing, just show prompt again — do NOT block or pause stream
319
+ rl.on("line", async (rawLine) => {
320
+ // Decode paste encoding before doing anything else
321
+ const text = restorePaste(rawLine).trim();
322
+
323
+ if (!text) { showPrompt(); return; }
324
+
325
+ // Don't queue — just warn and re-prompt
317
326
  if (processing) {
318
327
  printWarning("still thinking…");
319
328
  showPrompt();
320
329
  return;
321
330
  }
322
331
 
323
- const text = input.trim();
324
- if (!text) { showPrompt(); return; }
325
-
326
332
  processing = true;
327
333
  try {
328
334
  if (text.startsWith("/")) await handleCommand(text);
329
- else await send(input); // preserve original (may have \n from paste)
335
+ else await send(rawLine); // send rawLine; send() decodes internally
330
336
  } catch (e) {
331
337
  printError(e.message);
332
338
  } finally {
@@ -335,14 +341,9 @@ async function main() {
335
341
  }
336
342
  });
337
343
 
338
- // 'close' fires on Ctrl+D exit gracefully
339
- rl.on("close", () => {
340
- cleanup();
341
- console.log("");
342
- process.exit(0);
343
- });
344
+ // readline 'close' = Ctrl+D via our manual handler, or rl.close() call
345
+ rl.on("close", () => { cleanup(); console.log(""); process.exit(0); });
344
346
 
345
- // SIGINT (Ctrl+C) — cancel current operation or exit
346
347
  rl.on("SIGINT", () => {
347
348
  if (processing) {
348
349
  process.stdout.write("\n");
@@ -350,14 +351,8 @@ async function main() {
350
351
  showPrompt();
351
352
  return;
352
353
  }
353
- // Not processing — treat as exit intent
354
- cleanup();
355
- console.log("");
356
- process.exit(0);
354
+ cleanup(); console.log(""); process.exit(0);
357
355
  });
358
356
  }
359
357
 
360
- main().catch(e => {
361
- console.error(chalk.red("fatal:"), e.message);
362
- process.exit(1);
363
- });
358
+ 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.5",
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 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"); }