@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.
- package/index.js +110 -80
- package/package.json +1 -1
- package/src/agent.js +19 -20
- 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
|
-
|
|
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
|
-
|
|
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
|
|
52
|
+
function sysInstruction() {
|
|
55
53
|
return [memoryContext, sessionSystem].filter(Boolean).join("\n\n") || null;
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
// ─────────────────────────────────────────────────────────────────
|
|
59
|
-
// Readline —
|
|
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:
|
|
69
|
-
output:
|
|
70
|
-
terminal:
|
|
60
|
+
input: process.stdin,
|
|
61
|
+
output: process.stdout,
|
|
62
|
+
terminal: true,
|
|
71
63
|
historySize: 200,
|
|
64
|
+
crlfDelay: Infinity,
|
|
72
65
|
});
|
|
73
66
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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("
|
|
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(
|
|
94
|
-
const p = path.resolve(
|
|
95
|
-
if (!fs.existsSync(p))
|
|
96
|
-
if (fs.statSync(p).size > 20*1024*1024)
|
|
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(
|
|
106
|
-
|
|
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:
|
|
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 (
|
|
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
|
|
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
|
|
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)}
|
|
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
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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
|
|
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
|
|
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,
|
|
237
|
-
console.log(chalk.dim(
|
|
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:
|
|
248
|
-
messages: history
|
|
253
|
+
system: sysInstruction(),
|
|
254
|
+
messages: history,
|
|
249
255
|
}, null, 2));
|
|
250
|
-
printSuccess(`exported
|
|
256
|
+
printSuccess(`exported → ${arg}`);
|
|
251
257
|
break;
|
|
252
258
|
|
|
253
|
-
case "/cwd":
|
|
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)
|
|
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
|
-
|
|
277
|
+
cleanup();
|
|
278
|
+
printInfo("bye");
|
|
279
|
+
process.exit(0);
|
|
280
|
+
break;
|
|
271
281
|
|
|
272
282
|
default:
|
|
273
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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 (
|
|
318
|
-
else
|
|
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
|
-
|
|
322
|
-
prompt();
|
|
334
|
+
showPrompt();
|
|
323
335
|
}
|
|
324
336
|
});
|
|
325
337
|
|
|
326
|
-
|
|
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) {
|
|
329
|
-
|
|
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 => {
|
|
360
|
+
main().catch(e => {
|
|
361
|
+
console.error(chalk.red("fatal:"), e.message);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
});
|
package/package.json
CHANGED
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:
|
|
38
|
-
spinner:
|
|
39
|
-
color:
|
|
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
|
-
"
|
|
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.
|
|
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.
|
|
120
|
-
chalk.hex("#569CD6")
|
|
121
|
-
(preview ? chalk.
|
|
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"
|
|
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.
|
|
129
|
-
const lines = text.split("\n").slice(0,
|
|
130
|
-
const more = text.split("\n").length >
|
|
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
|
|
1
|
+
// src/input.js — Bracketed paste via keypress interception
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
|
7
|
+
import readline from "readline";
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
cb();
|
|
73
|
-
}
|
|
43
|
+
process.stdin.on("keypress", onKeypress);
|
|
74
44
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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"); }
|