@ikyyofc/gemini-cli 1.0.1 → 1.0.3

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 (4) hide show
  1. package/index.js +110 -80
  2. package/package.json +1 -1
  3. package/src/agent.js +19 -20
  4. package/src/input.js +50 -78
package/index.js CHANGED
@@ -19,8 +19,7 @@ import {
19
19
  printError, printInfo, printSuccess, printWarning
20
20
  } from "./src/renderer.js";
21
21
  import {
22
- PasteTransform, restorePaste,
23
- enableBracketedPaste, disableBracketedPaste
22
+ enableBracketedPaste, disableBracketedPaste, setupPaste
24
23
  } from "./src/input.js";
25
24
 
26
25
  // ─────────────────────────────────────────────────────────────────
@@ -34,8 +33,7 @@ let memoryContext = null;
34
33
 
35
34
  function reloadAll() {
36
35
  extensions = loadExtensions();
37
- const extra = getExtensionContextDirs(extensions);
38
- memoryLoaded = loadMemory(extra);
36
+ memoryLoaded = loadMemory(getExtensionContextDirs(extensions));
39
37
  memoryContext = buildContextString(memoryLoaded);
40
38
  }
41
39
  reloadAll();
@@ -51,49 +49,59 @@ let processing = false;
51
49
  let agentMode = true;
52
50
  let autoApprove = false;
53
51
 
54
- function systemInstruction() {
52
+ function sysInstruction() {
55
53
  return [memoryContext, sessionSystem].filter(Boolean).join("\n\n") || null;
56
54
  }
57
55
 
58
56
  // ─────────────────────────────────────────────────────────────────
59
- // Readline — pipe stdin through PasteTransform first
57
+ // Readline — directly on process.stdin, NO transform stream
60
58
  // ─────────────────────────────────────────────────────────────────
61
- enableBracketedPaste();
62
- process.on("exit", disableBracketedPaste);
63
-
64
- const pasteStream = new PasteTransform();
65
- process.stdin.pipe(pasteStream);
66
-
67
59
  const rl = readline.createInterface({
68
- input: pasteStream,
69
- output: process.stdout,
70
- terminal: true,
60
+ input: process.stdin,
61
+ output: process.stdout,
62
+ terminal: true,
71
63
  historySize: 200,
64
+ crlfDelay: Infinity,
72
65
  });
73
66
 
74
- function prompt() {
75
- const mode = agentMode ? chalk.hex("#4EC9B0")("agent") : chalk.dim("chat");
76
- const yolo = autoApprove ? chalk.red(" yolo") : "";
77
- const file = pendingFile ? chalk.yellow(` +${path.basename(pendingPath)}`) : "";
78
- const turns = history.length ? chalk.dim(` ${Math.ceil(history.length/2)}t`) : "";
79
- const mem = memoryLoaded.length ? chalk.dim(` m${memoryLoaded.length}`) : "";
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
+ }
77
+ }
78
+ process.on("exit", cleanup);
79
+ process.on("SIGTERM", () => { cleanup(); process.exit(0); });
80
+
81
+ // ─────────────────────────────────────────────────────────────────
82
+ // Prompt
83
+ // ─────────────────────────────────────────────────────────────────
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}`) : "";
80
90
 
81
91
  rl.setPrompt(
82
- "\n" +
83
- chalk.dim(" ") + chalk.bold("") + " " +
84
- chalk.dim("[") + mode + yolo + file + turns + mem + chalk.dim("]") +
85
- " "
92
+ "\n " + chalk.bold("❯") + " " +
93
+ chalk.dim("[") + mode + yolo + file + turns + mem + chalk.dim("]") + " "
86
94
  );
87
- rl.prompt();
95
+ rl.prompt(true);
88
96
  }
89
97
 
90
98
  // ─────────────────────────────────────────────────────────────────
91
99
  // File attachment
92
100
  // ─────────────────────────────────────────────────────────────────
93
- function attachFile(filePath) {
94
- const p = path.resolve(filePath.trim().replace(/^['"]|['"]$/g, ""));
95
- if (!fs.existsSync(p)) { printError(`not found: ${p}`); return; }
96
- if (fs.statSync(p).size > 20*1024*1024) { printError("file too large (max 20MB)"); return; }
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; }
97
105
  pendingFile = fs.readFileSync(p);
98
106
  pendingPath = p;
99
107
  printSuccess(`attached ${path.basename(p)}`);
@@ -102,16 +110,14 @@ function attachFile(filePath) {
102
110
  // ─────────────────────────────────────────────────────────────────
103
111
  // Send message
104
112
  // ─────────────────────────────────────────────────────────────────
105
- async function send(rawInput) {
106
- // restorePaste turns \x00 back into \n (from bracketed paste)
107
- const userText = restorePaste(rawInput).trim();
108
- if (!userText) return;
113
+ async function send(userText) {
114
+ if (!userText.trim()) return;
109
115
 
110
116
  printUser(userText + (pendingFile ? chalk.dim(` [${path.basename(pendingPath)}]`) : ""));
111
117
 
112
118
  if (agentMode) {
113
119
  const res = await runAgentLoop(userText, history, {
114
- systemInstruction: systemInstruction(),
120
+ systemInstruction: sysInstruction(),
115
121
  autoApprove,
116
122
  maxIterations: 40,
117
123
  });
@@ -121,9 +127,9 @@ async function send(rawInput) {
121
127
  }
122
128
  } else {
123
129
  const { default: ora } = await import("ora");
124
- const sp = ora({ text: "thinking…", spinner: "dots", color: "cyan", prefixText: " " }).start();
130
+ const sp = ora({ text: "thinking…", spinner: "dots", color: "cyan", prefixText: " ", discardStdin: false }).start();
125
131
  const msgs = [];
126
- if (systemInstruction()) msgs.push({ role: "system", content: systemInstruction() });
132
+ if (sysInstruction()) msgs.push({ role: "system", content: sysInstruction() });
127
133
  msgs.push(...history, { role: "user", content: userText });
128
134
  try {
129
135
  const t0 = Date.now();
@@ -144,15 +150,14 @@ async function send(rawInput) {
144
150
  // ─────────────────────────────────────────────────────────────────
145
151
  // Commands
146
152
  // ─────────────────────────────────────────────────────────────────
147
- async function command(input) {
153
+ async function handleCommand(input) {
148
154
  const tokens = input.trim().split(/\s+/);
149
155
  const cmd = tokens[0].toLowerCase();
150
156
  const arg = tokens.slice(1).join(" ").trim();
151
157
 
152
- // /memory
153
158
  if (cmd === "/memory") {
154
159
  const sub = tokens[1];
155
- if (sub === "show") { console.log(memoryShow(memoryLoaded)); }
160
+ if (sub === "show") { console.log(memoryShow(memoryLoaded)); }
156
161
  else if (sub === "reload") { reloadAll(); printSuccess(`reloaded — ${memoryLoaded.length} file(s)`); }
157
162
  else if (sub === "add") {
158
163
  const t = tokens.slice(2).join(" ");
@@ -162,7 +167,6 @@ async function command(input) {
162
167
  return;
163
168
  }
164
169
 
165
- // /ext
166
170
  if (cmd === "/ext") {
167
171
  const sub = tokens[1];
168
172
  const name = tokens[2];
@@ -172,29 +176,29 @@ async function command(input) {
172
176
  console.log("");
173
177
  list.forEach(e => {
174
178
  const s = e.enabled ? chalk.green("✓") : chalk.dim("○");
175
- console.log(` ${s} ${chalk.bold(e.name)} ${chalk.dim("v"+e.version)} ${chalk.dim(e.description)}`);
179
+ console.log(` ${s} ${chalk.bold(e.name)} ${chalk.dim("v"+e.version)} ${chalk.dim(e.description)}`);
176
180
  });
177
181
  console.log("");
178
182
  } else if (sub === "install") {
179
183
  const src = tokens.slice(2).join(" ");
180
184
  if (!src) { printError("usage: /ext install <path-or-url>"); return; }
181
- printInfo(`installing ${src}…`);
185
+ printInfo(`installing…`);
182
186
  const r = await installExtension(src);
183
187
  r.error ? printError(r.error) : (printSuccess(`installed: ${r.name}`), reloadAll());
184
188
  } else if (sub === "uninstall") {
185
- if (!name) { printError("usage: /ext uninstall <name>"); return; }
189
+ if (!name) { printError("usage: /ext uninstall <n>"); return; }
186
190
  const r = await uninstallExtension(name);
187
191
  r.error ? printError(r.error) : (printSuccess(`removed: ${name}`), reloadAll());
188
192
  } else if (sub === "enable") {
189
- if (!name) { printError("usage: /ext enable <name>"); return; }
193
+ if (!name) { printError("usage: /ext enable <n>"); return; }
190
194
  const r = enableExtension(name, true);
191
195
  r.error ? printError(r.error) : (printSuccess(`enabled: ${name}`), reloadAll());
192
196
  } else if (sub === "disable") {
193
- if (!name) { printError("usage: /ext disable <name>"); return; }
197
+ if (!name) { printError("usage: /ext disable <n>"); return; }
194
198
  const r = enableExtension(name, false);
195
199
  r.error ? printError(r.error) : (printSuccess(`disabled: ${name}`), reloadAll());
196
200
  } else if (sub === "update") {
197
- if (!name) { printError("usage: /ext update <name>"); return; }
201
+ if (!name) { printError("usage: /ext update <n>"); return; }
198
202
  printInfo(`updating ${name}…`);
199
203
  const r = await updateExtension(name);
200
204
  r.error ? printError(r.error) : (printSuccess(`updated: ${name}`), reloadAll());
@@ -220,12 +224,14 @@ async function command(input) {
220
224
 
221
225
  case "/agent":
222
226
  agentMode = !agentMode;
223
- printInfo(`mode: ${agentMode ? chalk.hex("#4EC9B0")("agent") + " (tools on)" : chalk.dim("chat") + " (tools off)"}`);
227
+ printInfo(`mode ${agentMode ? chalk.hex("#4EC9B0")("agent") : chalk.dim("chat")}`);
224
228
  break;
225
229
 
226
230
  case "/yolo":
227
231
  autoApprove = !autoApprove;
228
- autoApprove ? printWarning("yolo on — no confirmations") : printInfo("yolo off — confirmations restored");
232
+ autoApprove
233
+ ? printWarning("yolo on — all tool actions auto-approved")
234
+ : printInfo("yolo off — confirmations restored");
229
235
  break;
230
236
 
231
237
  case "/history":
@@ -233,8 +239,8 @@ async function command(input) {
233
239
  console.log("");
234
240
  history.forEach((m, i) => {
235
241
  const who = m.role === "user" ? chalk.hex("#4A9EFF")("you") : chalk.hex("#4EC9B0")("gemini");
236
- const body = String(m.content).replace(/\n/g," ").slice(0, 80);
237
- console.log(chalk.dim(` [${i+1}] `) + who + chalk.dim(": ") + body);
242
+ const body = String(m.content).replace(/\n/g, " ").slice(0, 90);
243
+ console.log(` ${chalk.dim("["+( i+1)+"]")} ${who} ${chalk.dim(body)}`);
238
244
  });
239
245
  console.log("");
240
246
  break;
@@ -244,13 +250,14 @@ async function command(input) {
244
250
  fs.writeFileSync(path.resolve(arg), JSON.stringify({
245
251
  exported_at: new Date().toISOString(),
246
252
  mode: agentMode ? "agent" : "chat",
247
- system: systemInstruction(),
248
- messages: history
253
+ system: sysInstruction(),
254
+ messages: history,
249
255
  }, null, 2));
250
- printSuccess(`exported to ${arg}`);
256
+ printSuccess(`exported ${arg}`);
251
257
  break;
252
258
 
253
- case "/cwd": printInfo(process.cwd()); break;
259
+ case "/cwd":
260
+ printInfo(process.cwd()); break;
254
261
 
255
262
  case "/cd":
256
263
  if (!arg) { printError("usage: /cd <path>"); break; }
@@ -262,31 +269,29 @@ async function command(input) {
262
269
  printInfo(`model gemini-pro-latest`);
263
270
  printInfo(`tools ${agentMode ? "on (native function calling)" : "off"}`);
264
271
  printInfo(`yolo ${autoApprove ? "on" : "off"}`);
265
- printInfo(`memory ${memoryLoaded.length} file(s) | extensions: ${extensions.length}`);
272
+ printInfo(`memory ${memoryLoaded.length} file(s) · extensions: ${extensions.length}`);
266
273
  printInfo(`config ${GLOBAL_DIR}`);
267
274
  break;
268
275
 
269
276
  case "/exit": case "/quit":
270
- printInfo("bye"); rl.close(); process.exit(0); break;
277
+ cleanup();
278
+ printInfo("bye");
279
+ process.exit(0);
280
+ break;
271
281
 
272
282
  default:
273
- // Custom extension commands: /namespace:cmd <args>
274
- if (cmd.slice(1).includes(":")) {
275
- printError(`unknown command: ${cmd} — type /help`);
276
- } else {
277
- printError(`unknown command: ${cmd} — type /help`);
278
- }
283
+ printError(`unknown command: ${cmd} — /help for list`);
279
284
  }
280
285
  }
281
286
 
282
287
  // ─────────────────────────────────────────────────────────────────
283
- // Main
288
+ // Main REPL loop
284
289
  // ─────────────────────────────────────────────────────────────────
285
290
  async function main() {
286
291
  process.stdout.write("\x1Bc");
287
292
  console.log(renderWelcome(memoryLoaded.length, extensions.length));
288
293
 
289
- // Parse CLI flags
294
+ // Parse flags
290
295
  const argv = process.argv.slice(2);
291
296
  const positional = [];
292
297
  for (let i = 0; i < argv.length; i++) {
@@ -294,40 +299,65 @@ async function main() {
294
299
  if (a === "--system" && argv[i+1]) { sessionSystem = argv[++i]; }
295
300
  else if (a === "--file" && argv[i+1]) { attachFile(argv[++i]); }
296
301
  else if (a === "--yolo") { autoApprove = true; printWarning("yolo on"); }
297
- else if (a === "--chat") { agentMode = false; }
302
+ else if (a === "--chat") { agentMode = false; }
298
303
  else if (!a.startsWith("--")) { positional.push(a); }
299
304
  }
300
305
 
301
306
  // One-shot mode
302
307
  if (positional.length > 0) {
303
308
  await send(positional.join(" "));
309
+ cleanup();
304
310
  process.exit(0);
305
311
  }
306
312
 
307
- prompt();
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
+ }
308
322
 
309
- rl.on("line", async raw => {
310
- const input = restorePaste(raw).trim();
311
- if (!input) { prompt(); return; }
312
- if (processing) { printWarning("still thinking…"); prompt(); return; }
323
+ const text = input.trim();
324
+ if (!text) { showPrompt(); return; }
313
325
 
314
326
  processing = true;
315
- rl.pause();
316
327
  try {
317
- if (input.startsWith("/")) await command(input);
318
- else await send(raw); // pass raw so send() can restorePaste
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);
319
332
  } finally {
320
333
  processing = false;
321
- rl.resume();
322
- prompt();
334
+ showPrompt();
323
335
  }
324
336
  });
325
337
 
326
- rl.on("close", () => { console.log(""); process.exit(0); });
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
327
346
  rl.on("SIGINT", () => {
328
- if (processing) { printWarning("press Ctrl+C again to force quit"); return; }
329
- rl.close();
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);
330
357
  });
331
358
  }
332
359
 
333
- main().catch(e => { console.error(chalk.red("fatal:"), e.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.1",
3
+ "version": "1.0.3",
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
@@ -34,10 +34,11 @@ export async function runAgentLoop(userMessage, history, {
34
34
 
35
35
  // ── Call Gemini API with tools ──────────────────────────
36
36
  const spinner = ora({
37
- text: chalk.hex("#858585")(iteration === 1 ? " Thinking…" : ` Step ${iteration}…`),
38
- spinner: "dots12",
39
- color: "cyan",
40
- prefixText: " "
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
41
42
  }).start();
42
43
 
43
44
  let parts;
@@ -60,9 +61,7 @@ export async function runAgentLoop(userMessage, history, {
60
61
  const textContent = textParts.map(p => p.text).join("").trim();
61
62
  if (textContent && callParts.length > 0) {
62
63
  process.stdout.write(
63
- "\n" + chalk.hex("#858585").italic(
64
- " 💭 " + textContent.replace(/\n/g, "\n ")
65
- ) + "\n\n"
64
+ chalk.dim("" + textContent.replace(/\n/g, "\n ")) + "\n"
66
65
  );
67
66
  }
68
67
 
@@ -110,32 +109,32 @@ function printToolCall(name, args = {}) {
110
109
  const preview = Object.entries(args)
111
110
  .map(([k, v]) => {
112
111
  const s = String(v);
113
- return chalk.hex("#9CDCFE")(k) + chalk.hex("#858585")(":") +
114
- chalk.hex("#CE9178")(s.length > 60 ? s.slice(0, 60) + "…" : s);
112
+ return chalk.dim(k + ":") + (s.length > 60 ? s.slice(0, 60) + "" : s);
115
113
  })
116
114
  .join(" ");
117
115
 
118
116
  process.stdout.write(
119
- chalk.hex("#DCDCAA")(" ") +
120
- chalk.hex("#569CD6").bold(name) +
121
- (preview ? chalk.hex("#858585")(" (" + preview + ")") : "") + "\n"
117
+ " " + chalk.dim("run") + " " +
118
+ chalk.hex("#569CD6")(name) +
119
+ (preview ? chalk.dim(" " + preview) : "") + "\n"
122
120
  );
123
121
  }
124
122
 
125
123
  function printToolResult(result, name) {
126
- const text = typeof result === "object" ? (result.result ?? result.error ?? JSON.stringify(result)) : String(result);
124
+ const text = typeof result === "object"
125
+ ? (result.result ?? result.error ?? JSON.stringify(result))
126
+ : String(result);
127
127
  const isErr = typeof result === "object" && result.error;
128
- const color = isErr ? chalk.hex("#F44747") : chalk.hex("#6A9955");
129
- const lines = text.split("\n").slice(0, 15);
130
- const more = text.split("\n").length > 15 ? `\n … (+${text.split("\n").length - 15} lines)` : "";
128
+ const color = isErr ? chalk.red : chalk.dim;
129
+ const lines = text.split("\n").slice(0, 12);
130
+ const more = text.split("\n").length > 12
131
+ ? chalk.dim(`\n … +${text.split("\n").length - 12} lines`) : "";
131
132
 
132
133
  process.stdout.write(
133
- color("") +
134
- chalk.hex("#858585")(lines.join("\n ")) +
135
- more + "\n\n"
134
+ " " + color(lines.join("\n ")) + more + "\n"
136
135
  );
137
136
  }
138
137
 
139
138
  function clearLine() {
140
139
  if (process.stdout.clearLine) { process.stdout.clearLine(0); process.stdout.cursorTo(0); }
141
- }
140
+ }
package/src/input.js CHANGED
@@ -1,88 +1,60 @@
1
- // src/input.js — Bracketed paste mode via Transform stream
1
+ // src/input.js — Bracketed paste via keypress interception
2
2
  //
3
- // How it works:
4
- // Terminal sends: \x1b[200~ <pasted text with \n> \x1b[201~
5
- // We detect start/end, collect paste buffer, replace \n with \x00
6
- // so readline treats it as ONE line, then restore \n in the handler.
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
7
6
  //
8
- import { Transform } from "stream";
7
+ import readline from "readline";
9
8
 
10
- const PASTE_START = "\x1b[200~";
11
- const PASTE_END = "\x1b[201~";
12
-
13
- export class PasteTransform extends Transform {
14
- constructor() {
15
- super();
16
- this._pasting = false;
17
- this._pasteBuf = "";
18
- this._raw = ""; // accumulate raw bytes for sequence detection
19
- }
20
-
21
- _transform(chunk, _enc, cb) {
22
- this._raw += chunk.toString("utf8");
23
-
24
- let out = "";
25
-
26
- while (this._raw.length > 0) {
27
- // ── Paste START ──────────────────────────────────────────
28
- const si = this._raw.indexOf(PASTE_START);
29
- if (!this._pasting && si !== -1) {
30
- // pass through everything before the sequence
31
- out += this._raw.slice(0, si);
32
- this._pasting = true;
33
- this._pasteBuf = "";
34
- this._raw = this._raw.slice(si + PASTE_START.length);
35
- continue;
36
- }
37
-
38
- // ── Paste END ────────────────────────────────────────────
39
- const ei = this._raw.indexOf(PASTE_END);
40
- if (this._pasting && ei !== -1) {
41
- // collect everything up to end marker
42
- this._pasteBuf += this._raw.slice(0, ei);
43
- this._raw = this._raw.slice(ei + PASTE_END.length);
44
- this._pasting = false;
45
-
46
- // Replace newlines with \x00 so readline sees ONE line
47
- // (\x00 is a null byte readline won't split on)
48
- const safe = this._pasteBuf.replace(/\r\n/g, "\x00").replace(/\n/g, "\x00");
49
- out += safe + "\n"; // the trailing \n makes readline fire one line event
50
- this._pasteBuf = "";
51
- continue;
52
- }
53
-
54
- // ── Inside paste, no end yet — wait for more data ────────
55
- if (this._pasting) {
56
- this._pasteBuf += this._raw;
57
- this._raw = "";
58
- break;
59
- }
9
+ export function enableBracketedPaste() { process.stdout.write("\x1b[?2004h"); }
10
+ export function disableBracketedPaste() { process.stdout.write("\x1b[?2004l"); }
60
11
 
61
- // ── Partial escape sequence at end — wait for more data ──
62
- if (this._raw.startsWith("\x1b") && this._raw.length < PASTE_START.length) {
63
- break;
64
- }
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
+ }
65
33
 
66
- // ── Normal data ──────────────────────────────────────────
67
- out += this._raw;
68
- this._raw = "";
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);
69
40
  }
41
+ };
70
42
 
71
- if (out) this.push(out);
72
- cb();
73
- }
43
+ process.stdin.on("keypress", onKeypress);
74
44
 
75
- _flush(cb) {
76
- if (this._pasteBuf) this.push(this._pasteBuf);
77
- cb();
78
- }
79
- }
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
+ };
80
55
 
81
- /** Restore \x00 → \n in a line received from readline */
82
- export function restorePaste(line) {
83
- return line.replace(/\x00/g, "\n");
56
+ return () => {
57
+ process.stdin.off("keypress", onKeypress);
58
+ rl.emit = _emit;
59
+ };
84
60
  }
85
-
86
- /** Enable/disable bracketed paste mode on the terminal */
87
- export function enableBracketedPaste() { process.stdout.write("\x1b[?2004h"); }
88
- export function disableBracketedPaste() { process.stdout.write("\x1b[?2004l"); }