@iinm/plain-agent 1.5.3 → 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.
@@ -539,6 +539,15 @@
539
539
  }
540
540
  }
541
541
  }
542
+ },
543
+ "cost": {
544
+ "currency": "USD",
545
+ "unit": "1M",
546
+ "costs": {
547
+ "promptTokenCount": 2,
548
+ "cachedContentTokenCount": -1.8,
549
+ "candidatesTokenCount": 12
550
+ }
542
551
  }
543
552
  },
544
553
  {
@@ -565,6 +574,15 @@
565
574
  }
566
575
  }
567
576
  }
577
+ },
578
+ "cost": {
579
+ "currency": "USD",
580
+ "unit": "1M",
581
+ "costs": {
582
+ "promptTokenCount": 2,
583
+ "cachedContentTokenCount": -1.8,
584
+ "candidatesTokenCount": 12
585
+ }
568
586
  }
569
587
  },
570
588
 
@@ -659,6 +677,15 @@
659
677
  }
660
678
  }
661
679
  }
680
+ },
681
+ "cost": {
682
+ "currency": "USD",
683
+ "unit": "1M",
684
+ "costs": {
685
+ "promptTokenCount": 2,
686
+ "cachedContentTokenCount": -1.8,
687
+ "candidatesTokenCount": 12
688
+ }
662
689
  }
663
690
  },
664
691
  {
@@ -684,6 +711,15 @@
684
711
  }
685
712
  }
686
713
  }
714
+ },
715
+ "cost": {
716
+ "currency": "USD",
717
+ "unit": "1M",
718
+ "costs": {
719
+ "promptTokenCount": 2,
720
+ "cachedContentTokenCount": -1.8,
721
+ "candidatesTokenCount": 12
722
+ }
687
723
  }
688
724
  },
689
725
 
@@ -703,6 +739,15 @@
703
739
  "store": false,
704
740
  "include": ["reasoning.encrypted_content"]
705
741
  }
742
+ },
743
+ "cost": {
744
+ "currency": "USD",
745
+ "unit": "1M",
746
+ "costs": {
747
+ "input_tokens": 0.75,
748
+ "cached_tokens": -0.675,
749
+ "output_tokens": 4.5
750
+ }
706
751
  }
707
752
  },
708
753
  {
@@ -721,6 +766,15 @@
721
766
  "store": false,
722
767
  "include": ["reasoning.encrypted_content"]
723
768
  }
769
+ },
770
+ "cost": {
771
+ "currency": "USD",
772
+ "unit": "1M",
773
+ "costs": {
774
+ "input_tokens": 0.75,
775
+ "cached_tokens": -0.675,
776
+ "output_tokens": 4.5
777
+ }
724
778
  }
725
779
  },
726
780
  {
@@ -739,6 +793,15 @@
739
793
  "store": false,
740
794
  "include": ["reasoning.encrypted_content"]
741
795
  }
796
+ },
797
+ "cost": {
798
+ "currency": "USD",
799
+ "unit": "1M",
800
+ "costs": {
801
+ "input_tokens": 0.75,
802
+ "cached_tokens": -0.675,
803
+ "output_tokens": 4.5
804
+ }
742
805
  }
743
806
  },
744
807
  {
@@ -757,6 +820,15 @@
757
820
  "store": false,
758
821
  "include": ["reasoning.encrypted_content"]
759
822
  }
823
+ },
824
+ "cost": {
825
+ "currency": "USD",
826
+ "unit": "1M",
827
+ "costs": {
828
+ "input_tokens": 2.5,
829
+ "cached_tokens": -2.25,
830
+ "output_tokens": 15
831
+ }
760
832
  }
761
833
  },
762
834
  {
@@ -775,6 +847,15 @@
775
847
  "store": false,
776
848
  "include": ["reasoning.encrypted_content"]
777
849
  }
850
+ },
851
+ "cost": {
852
+ "currency": "USD",
853
+ "unit": "1M",
854
+ "costs": {
855
+ "input_tokens": 2.5,
856
+ "cached_tokens": -2.25,
857
+ "output_tokens": 15
858
+ }
778
859
  }
779
860
  },
780
861
  {
@@ -793,6 +874,15 @@
793
874
  "store": false,
794
875
  "include": ["reasoning.encrypted_content"]
795
876
  }
877
+ },
878
+ "cost": {
879
+ "currency": "USD",
880
+ "unit": "1M",
881
+ "costs": {
882
+ "input_tokens": 2.5,
883
+ "cached_tokens": -2.25,
884
+ "output_tokens": 15
885
+ }
796
886
  }
797
887
  },
798
888
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.5.3",
3
+ "version": "1.6.0",
4
4
  "description": "A lightweight CLI-based coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -219,12 +219,18 @@ export function formatCostSummary(summary) {
219
219
 
220
220
  const lines = [];
221
221
 
222
- // Header
223
- lines.push(styleText("bold", "\nSession Cost Summary\n"));
224
-
225
- // Tokens
226
- lines.push(styleText("bold", "Tokens:"));
222
+ if (summary.totalCost !== undefined) {
223
+ lines.push(
224
+ styleText(
225
+ "bold",
226
+ `\nTotal: ${summary.totalCost.toFixed(4)} ${summary.currency}`,
227
+ ),
228
+ );
229
+ } else {
230
+ lines.push(styleText("yellow", "Total: N/A (no cost configuration)"));
231
+ }
227
232
 
233
+ lines.push(styleText("bold", "\nTokens:"));
228
234
  for (const [key, { tokens, cost }] of Object.entries(summary.breakdown)) {
229
235
  const tokenStr = `${key}: ${tokens.toLocaleString()}`;
230
236
 
@@ -236,19 +242,6 @@ export function formatCostSummary(summary) {
236
242
  }
237
243
  }
238
244
 
239
- // Total
240
- lines.push("");
241
- if (summary.totalCost !== undefined) {
242
- lines.push(
243
- styleText(
244
- "bold",
245
- `Total: ${summary.totalCost.toFixed(4)} ${summary.currency}`,
246
- ),
247
- );
248
- } else {
249
- lines.push(styleText("yellow", "Total: N/A (no cost configuration)"));
250
- }
251
-
252
245
  return lines.join("\n");
253
246
  }
254
247
 
@@ -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,13 +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
- console.log(styleText("gray", "\n<agent>"));
238
- console.log(message);
239
- console.log(styleText("gray", "</agent>"));
288
+ const [goalTextContent, ...goalImages] = await loadUserMessageContext(goal);
289
+ const goalText =
290
+ goalTextContent?.type === "text" ? goalTextContent.text : goal;
240
291
 
241
- userEventEmitter.emit("userInput", [{ type: "text", text: message }]);
292
+ const messageText = `Delegate to "${name}" agent with goal: ${goalText}`;
293
+ userEventEmitter.emit("userInput", [
294
+ { type: "text", text: messageText },
295
+ ...goalImages,
296
+ ]);
242
297
  }
243
298
 
244
299
  /**
@@ -258,16 +313,21 @@ export function startInteractiveSession({
258
313
  return;
259
314
  }
260
315
 
261
- 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}` : ""}`;
262
323
  const message = prompt.isSkill
263
324
  ? `System: This prompt was invoked as "${invocation}".\nPrompt path: ${prompt.filePath}\n\n${prompt.content}`
264
325
  : `System: This prompt was invoked as "${invocation}".\n\n${prompt.content}`;
265
326
 
266
- console.log(styleText("gray", "\n<prompt>"));
267
- console.log(message);
268
- console.log(styleText("gray", "</prompt>"));
269
-
270
- userEventEmitter.emit("userInput", [{ type: "text", text: message }]);
327
+ userEventEmitter.emit("userInput", [
328
+ { type: "text", text: message },
329
+ ...argsImages,
330
+ ]);
271
331
  }
272
332
 
273
333
  const getCliPrompt = (subagentName = "") =>
@@ -283,9 +343,89 @@ export function startInteractiveSession({
283
343
  "> ",
284
344
  ].join("\n");
285
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
+
286
426
  let currentCliPrompt = getCliPrompt();
287
427
  const cli = readline.createInterface({
288
- input: process.stdin,
428
+ input: pasteTransform,
289
429
  output: process.stdout,
290
430
  prompt: currentCliPrompt,
291
431
  /**
@@ -372,16 +512,46 @@ export function startInteractiveSession({
372
512
  if (process.stdin.isTTY) {
373
513
  process.stdin.setRawMode(true);
374
514
  }
375
-
376
- process.stdin.on("keypress", async (_, key) => {
377
- if (key.ctrl && key.name === "c") {
378
- 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");
379
519
  }
520
+ };
380
521
 
381
- if (key.ctrl && key.name === "d") {
382
- 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;
383
545
  }
384
- });
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);
385
555
 
386
556
  /**
387
557
  * Process the complete user input.
@@ -392,7 +562,9 @@ export function startInteractiveSession({
392
562
  // Prevent concurrent input processing from multi-line paste
393
563
  state.turn = false;
394
564
 
395
- const inputTrimmed = input.trim();
565
+ // Resolve paste placeholders to original content
566
+ const resolvedInput = resolvePastePlaceholders(input);
567
+ const inputTrimmed = resolvedInput.trim();
396
568
 
397
569
  if (inputTrimmed.length === 0) {
398
570
  state.turn = true;
@@ -427,10 +599,6 @@ export function startInteractiveSession({
427
599
  return;
428
600
  }
429
601
 
430
- console.log(styleText("gray", "\n<input>"));
431
- console.log(fileContent);
432
- console.log(styleText("gray", "</input>"));
433
-
434
602
  const messageWithContext = await loadUserMessageContext(fileContent);
435
603
 
436
604
  userEventEmitter.emit("userInput", messageWithContext);
@@ -501,7 +669,7 @@ export function startInteractiveSession({
501
669
  }
502
670
 
503
671
  if (inputTrimmed.startsWith("/prompts:")) {
504
- const match = inputTrimmed.match(/^\/prompts:([^ ]+)(?:\s+(.*))?$/);
672
+ const match = inputTrimmed.match(/^\/prompts:([^ ]+)(?:\s+(.*))?$/s);
505
673
  if (!match) {
506
674
  console.log(styleText("red", "\nInvalid prompt invocation format."));
507
675
  state.turn = true;
@@ -514,7 +682,7 @@ export function startInteractiveSession({
514
682
  }
515
683
 
516
684
  if (inputTrimmed.startsWith("/agents:")) {
517
- const match = inputTrimmed.match(/^\/agents:([^ ]+)(?:\s+(.*))?$/);
685
+ const match = inputTrimmed.match(/^\/agents:([^ ]+)(?:\s+(.*))?$/s);
518
686
  if (!match) {
519
687
  console.log(styleText("red", "\nInvalid agent invocation format."));
520
688
  state.turn = true;
@@ -561,10 +729,6 @@ export function startInteractiveSession({
561
729
 
562
730
  const combinedInput = prompt ? `${prompt}\n\n${clipboard}` : clipboard;
563
731
 
564
- console.log(styleText("gray", "\n<paste>"));
565
- console.log(combinedInput);
566
- console.log(styleText("gray", "</paste>"));
567
-
568
732
  const messageWithContext = await loadUserMessageContext(combinedInput);
569
733
  userEventEmitter.emit("userInput", messageWithContext);
570
734
  return;
@@ -600,7 +764,7 @@ export function startInteractiveSession({
600
764
  return;
601
765
  }
602
766
 
603
- // Handle multi-line delimiter
767
+ // Check for multi-line delimiter
604
768
  if (lineInput.trim() === '"""') {
605
769
  if (state.multiLineBuffer === null) {
606
770
  state.multiLineBuffer = [];
@@ -697,6 +861,10 @@ export function startInteractiveSession({
697
861
  });
698
862
 
699
863
  cli.prompt();
864
+
865
+ // Register cleanup handlers
866
+ process.on("exit", cleanup);
867
+ process.on("SIGTERM", cleanup);
700
868
  }
701
869
 
702
870
  /**
@@ -742,6 +910,9 @@ function printMessage(message) {
742
910
  console.log(part.text);
743
911
  break;
744
912
  }
913
+ case "image": {
914
+ break;
915
+ }
745
916
  default: {
746
917
  console.log(styleText("bold", "\nUnknown Message Format:"));
747
918
  console.log(JSON.stringify(part, null, 2));