@iinm/plain-agent 1.5.4 → 1.6.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.5.4",
3
+ "version": "1.6.0",
4
4
  "description": "A lightweight CLI-based coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { execFileSync } from "node:child_process";
8
8
  import readline from "node:readline";
9
+ import { Transform } from "node:stream";
9
10
  import { styleText } from "node:util";
10
11
  import {
11
12
  formatCostSummary,
@@ -189,6 +190,57 @@ const HELP_MESSAGE = [
189
190
  .replace(/^ {2}\/.+?(?= - )/gm, (m) => styleText("cyan", m))
190
191
  .replace(/^ {2}.+?(?= - )/gm, (m) => styleText("blue", m));
191
192
 
193
+ // Bracketed paste mode sequences
194
+ const BRACKETED_PASTE_START = "\x1b[200~";
195
+ const BRACKETED_PASTE_END = "\x1b[201~";
196
+
197
+ // Store for pasted content
198
+ const pastedContentStore = new Map();
199
+
200
+ /**
201
+ * Generate a short hash for paste reference
202
+ * @param {string} content
203
+ * @returns {string}
204
+ */
205
+ function generatePasteHash(content) {
206
+ let hash = 0;
207
+ for (let i = 0; i < content.length; i++) {
208
+ const char = content.charCodeAt(i);
209
+ hash = (hash << 5) - hash + char;
210
+ hash = hash & hash; // Convert to 32bit integer
211
+ }
212
+ return Math.abs(hash).toString(16).padStart(6, "0").slice(0, 6);
213
+ }
214
+
215
+ /**
216
+ * Resolve paste placeholders and append context tags
217
+ * @param {string} input
218
+ * @returns {string}
219
+ */
220
+ function resolvePastePlaceholders(input) {
221
+ /** @type {string[]} */
222
+ const contexts = [];
223
+
224
+ // Collect paste content for context tags while keeping placeholders
225
+ const text = input.replace(/\[pasted#([a-f0-9]{6})\]/g, (match, hash) => {
226
+ const content = pastedContentStore.get(hash);
227
+ if (content !== undefined) {
228
+ pastedContentStore.delete(hash); // Clean up after use
229
+ contexts.push(
230
+ `<context location="pasted#${hash}">\n${content}\n</context>`,
231
+ );
232
+ }
233
+ return match; // Keep placeholder in text
234
+ });
235
+
236
+ // Append contexts to the end of input
237
+ if (contexts.length > 0) {
238
+ return [text, ...contexts].join("\n\n");
239
+ }
240
+
241
+ return text;
242
+ }
243
+
192
244
  /**
193
245
  * @typedef {object} CliOptions
194
246
  * @property {UserEventEmitter} userEventEmitter
@@ -232,9 +284,16 @@ export function startInteractiveSession({
232
284
  const agentRoles = await loadAgentRoles(claudeCodePlugins);
233
285
  const agent = agentRoles.get(id);
234
286
  const name = agent ? id : `custom:${id}`;
235
- const message = `Delegate to "${name}" agent with goal: ${goal}`;
236
287
 
237
- userEventEmitter.emit("userInput", [{ type: "text", text: message }]);
288
+ const [goalTextContent, ...goalImages] = await loadUserMessageContext(goal);
289
+ const goalText =
290
+ goalTextContent?.type === "text" ? goalTextContent.text : goal;
291
+
292
+ const messageText = `Delegate to "${name}" agent with goal: ${goalText}`;
293
+ userEventEmitter.emit("userInput", [
294
+ { type: "text", text: messageText },
295
+ ...goalImages,
296
+ ]);
238
297
  }
239
298
 
240
299
  /**
@@ -254,12 +313,21 @@ export function startInteractiveSession({
254
313
  return;
255
314
  }
256
315
 
257
- const invocation = `${displayInvocation}${args ? ` ${args}` : ""}`;
316
+ const [argsTextContent, ...argsImages] = args
317
+ ? await loadUserMessageContext(args)
318
+ : [];
319
+ const argsText =
320
+ argsTextContent?.type === "text" ? argsTextContent.text : args;
321
+
322
+ const invocation = `${displayInvocation}${argsText ? ` ${argsText}` : ""}`;
258
323
  const message = prompt.isSkill
259
324
  ? `System: This prompt was invoked as "${invocation}".\nPrompt path: ${prompt.filePath}\n\n${prompt.content}`
260
325
  : `System: This prompt was invoked as "${invocation}".\n\n${prompt.content}`;
261
326
 
262
- userEventEmitter.emit("userInput", [{ type: "text", text: message }]);
327
+ userEventEmitter.emit("userInput", [
328
+ { type: "text", text: message },
329
+ ...argsImages,
330
+ ]);
263
331
  }
264
332
 
265
333
  const getCliPrompt = (subagentName = "") =>
@@ -275,9 +343,89 @@ export function startInteractiveSession({
275
343
  "> ",
276
344
  ].join("\n");
277
345
 
346
+ // Indirect reference for exit handler (assigned after confirmExit is defined)
347
+ let onExitRequest = () => {};
348
+
349
+ // Create a transform stream to handle bracketed paste before readline
350
+ let inPasteMode = false;
351
+ let pasteBuffer = "";
352
+
353
+ const pasteTransform = new Transform({
354
+ transform(chunk, _encoding, callback) {
355
+ let data = chunk.toString("utf8");
356
+
357
+ // Handle Ctrl-C and Ctrl-D
358
+ if (data.includes("\x03") || data.includes("\x04")) {
359
+ // Ctrl-C / Ctrl-D: request exit (handled by confirmExit)
360
+ onExitRequest();
361
+ callback();
362
+ return;
363
+ }
364
+
365
+ while (data.length > 0) {
366
+ if (inPasteMode) {
367
+ const endIdx = data.indexOf(BRACKETED_PASTE_END);
368
+ if (endIdx !== -1) {
369
+ // End of paste
370
+ pasteBuffer += data.slice(0, endIdx);
371
+ data = data.slice(endIdx + BRACKETED_PASTE_END.length);
372
+ inPasteMode = false;
373
+
374
+ // Handle paste content
375
+ if (pasteBuffer) {
376
+ // Remove trailing newline for single-line paste detection
377
+ const trimmedPaste = pasteBuffer.replace(/\n$/, "");
378
+
379
+ // For single-line paste, insert directly without placeholder
380
+ if (!trimmedPaste.includes("\n")) {
381
+ this.push(trimmedPaste);
382
+ } else {
383
+ // For multi-line paste, use placeholder
384
+ const hash = generatePasteHash(pasteBuffer);
385
+ pastedContentStore.set(hash, pasteBuffer);
386
+ this.push(`[pasted#${hash}] `);
387
+ }
388
+ }
389
+ pasteBuffer = "";
390
+ } else {
391
+ // Still in paste mode
392
+ pasteBuffer += data;
393
+ data = "";
394
+ }
395
+ } else {
396
+ const startIdx = data.indexOf(BRACKETED_PASTE_START);
397
+ if (startIdx !== -1) {
398
+ // Start of paste
399
+ // Output any data before the paste
400
+ if (startIdx > 0) {
401
+ this.push(data.slice(0, startIdx));
402
+ }
403
+ data = data.slice(startIdx + BRACKETED_PASTE_START.length);
404
+ inPasteMode = true;
405
+ pasteBuffer = "";
406
+ } else {
407
+ // Normal data
408
+ this.push(data);
409
+ data = "";
410
+ }
411
+ }
412
+ }
413
+
414
+ callback();
415
+ },
416
+ });
417
+
418
+ // Set up transformed stdin for readline
419
+ process.stdin.pipe(pasteTransform);
420
+
421
+ // Enable bracketed paste mode
422
+ if (process.stdout.isTTY) {
423
+ process.stdout.write("\x1b[?2004h");
424
+ }
425
+
278
426
  let currentCliPrompt = getCliPrompt();
279
427
  const cli = readline.createInterface({
280
- input: process.stdin,
428
+ input: pasteTransform,
281
429
  output: process.stdout,
282
430
  prompt: currentCliPrompt,
283
431
  /**
@@ -364,22 +512,46 @@ export function startInteractiveSession({
364
512
  if (process.stdin.isTTY) {
365
513
  process.stdin.setRawMode(true);
366
514
  }
367
-
368
- process.stdin.on("keypress", async (_, key) => {
369
- if (key.ctrl && key.name === "c") {
370
- const summary = agentCommands.getCostSummary();
371
- console.log();
372
- console.log(formatCostSummary(summary));
373
- await onStop();
515
+ // Cleanup handler to disable bracketed paste mode on exit
516
+ const cleanup = () => {
517
+ if (process.stdout.isTTY) {
518
+ process.stdout.write("\x1b[?2004l");
374
519
  }
520
+ };
375
521
 
376
- if (key.ctrl && key.name === "d") {
377
- const summary = agentCommands.getCostSummary();
378
- console.log();
379
- console.log(formatCostSummary(summary));
380
- await onStop();
522
+ // Handle exit signals
523
+ let isExiting = false;
524
+ const handleExit = async () => {
525
+ if (isExiting) return;
526
+ isExiting = true;
527
+
528
+ cleanup();
529
+ const summary = agentCommands.getCostSummary();
530
+ console.log();
531
+ console.log(formatCostSummary(summary));
532
+ await onStop();
533
+ process.exit(0);
534
+ };
535
+
536
+ // Double-press exit confirmation
537
+ let lastExitAttempt = 0;
538
+ const EXIT_CONFIRM_TIMEOUT = 1500;
539
+
540
+ const confirmExit = () => {
541
+ const now = Date.now();
542
+ if (now - lastExitAttempt < EXIT_CONFIRM_TIMEOUT) {
543
+ handleExit();
544
+ return;
381
545
  }
382
- });
546
+ lastExitAttempt = now;
547
+ console.log(styleText("yellow", "\nPress Ctrl-C or Ctrl-D again to exit."));
548
+ };
549
+
550
+ // Wire up exit request handler for Ctrl-C / Ctrl-D
551
+ onExitRequest = confirmExit;
552
+
553
+ // Handle readline close (e.g., stdin closed externally)
554
+ cli.on("close", handleExit);
383
555
 
384
556
  /**
385
557
  * Process the complete user input.
@@ -390,7 +562,9 @@ export function startInteractiveSession({
390
562
  // Prevent concurrent input processing from multi-line paste
391
563
  state.turn = false;
392
564
 
393
- const inputTrimmed = input.trim();
565
+ // Resolve paste placeholders to original content
566
+ const resolvedInput = resolvePastePlaceholders(input);
567
+ const inputTrimmed = resolvedInput.trim();
394
568
 
395
569
  if (inputTrimmed.length === 0) {
396
570
  state.turn = true;
@@ -495,7 +669,7 @@ export function startInteractiveSession({
495
669
  }
496
670
 
497
671
  if (inputTrimmed.startsWith("/prompts:")) {
498
- const match = inputTrimmed.match(/^\/prompts:([^ ]+)(?:\s+(.*))?$/);
672
+ const match = inputTrimmed.match(/^\/prompts:([^ ]+)(?:\s+(.*))?$/s);
499
673
  if (!match) {
500
674
  console.log(styleText("red", "\nInvalid prompt invocation format."));
501
675
  state.turn = true;
@@ -508,7 +682,7 @@ export function startInteractiveSession({
508
682
  }
509
683
 
510
684
  if (inputTrimmed.startsWith("/agents:")) {
511
- const match = inputTrimmed.match(/^\/agents:([^ ]+)(?:\s+(.*))?$/);
685
+ const match = inputTrimmed.match(/^\/agents:([^ ]+)(?:\s+(.*))?$/s);
512
686
  if (!match) {
513
687
  console.log(styleText("red", "\nInvalid agent invocation format."));
514
688
  state.turn = true;
@@ -590,7 +764,7 @@ export function startInteractiveSession({
590
764
  return;
591
765
  }
592
766
 
593
- // Handle multi-line delimiter
767
+ // Check for multi-line delimiter
594
768
  if (lineInput.trim() === '"""') {
595
769
  if (state.multiLineBuffer === null) {
596
770
  state.multiLineBuffer = [];
@@ -687,6 +861,10 @@ export function startInteractiveSession({
687
861
  });
688
862
 
689
863
  cli.prompt();
864
+
865
+ // Register cleanup handlers
866
+ process.on("exit", cleanup);
867
+ process.on("SIGTERM", cleanup);
690
868
  }
691
869
 
692
870
  /**