@bike4mind/cli 0.2.6-fix-cli-mcp-type-mismatch.17119 → 0.2.7-feat-cli-rewind-command.17128

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/dist/index.js CHANGED
@@ -79,8 +79,8 @@ import {
79
79
  } from "./chunk-PDX44BCA.js";
80
80
 
81
81
  // src/index.tsx
82
- import React13, { useState as useState4, useEffect as useEffect3, useCallback } from "react";
83
- import { render, Box as Box13, Text as Text12, useApp, useInput as useInput3 } from "ink";
82
+ import React14, { useState as useState5, useEffect as useEffect3, useCallback, useRef } from "react";
83
+ import { render, Box as Box14, Text as Text13, useApp, useInput as useInput4 } from "ink";
84
84
  import { v4 as uuidv49 } from "uuid";
85
85
 
86
86
  // src/components/App.tsx
@@ -389,6 +389,11 @@ var COMMANDS = [
389
389
  name: "clear",
390
390
  description: "Start a new session",
391
391
  aliases: ["new"]
392
+ },
393
+ {
394
+ name: "rewind",
395
+ description: "Rewind conversation to a previous point",
396
+ aliases: ["undo"]
392
397
  }
393
398
  ];
394
399
 
@@ -413,6 +418,202 @@ function searchCommands(query) {
413
418
  return results.map((result) => result.item);
414
419
  }
415
420
 
421
+ // src/utils/imageDetector.ts
422
+ import { readFileSync, existsSync, statSync as statSync2 } from "fs";
423
+ import { extname as extname2 } from "path";
424
+ var ImageInputDetector = class {
425
+ static {
426
+ this.MAX_IMAGE_SIZE = 10 * 1024 * 1024;
427
+ }
428
+ static {
429
+ // 10MB
430
+ this.SUPPORTED_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".webp"];
431
+ }
432
+ /**
433
+ * Check if input contains image data
434
+ */
435
+ static containsImageData(input) {
436
+ if (input.includes("\x1B]1337;File=")) {
437
+ return true;
438
+ }
439
+ if (input.includes("\x1B_G")) {
440
+ return true;
441
+ }
442
+ if (input.includes("\x1BP")) {
443
+ return true;
444
+ }
445
+ if (input.match(/data:image\/(png|jpeg|jpg|gif|webp);base64,/)) {
446
+ return true;
447
+ }
448
+ if (this.looksLikeFilePath(input)) {
449
+ return true;
450
+ }
451
+ return false;
452
+ }
453
+ /**
454
+ * Extract image data from terminal input
455
+ */
456
+ static extractImageData(input) {
457
+ const iterm2 = this.extractITerm2Image(input);
458
+ if (iterm2) return iterm2;
459
+ const kitty = this.extractKittyImage(input);
460
+ if (kitty) return kitty;
461
+ const sixel = this.extractSixelImage(input);
462
+ if (sixel) return sixel;
463
+ const base64 = this.extractBase64Image(input);
464
+ if (base64) return base64;
465
+ const fileDrop = this.extractDroppedFile(input);
466
+ if (fileDrop) return fileDrop;
467
+ return null;
468
+ }
469
+ /**
470
+ * Extract iTerm2 inline image
471
+ * Format: ESC]1337;File=[args]:base64data^G
472
+ */
473
+ static extractITerm2Image(input) {
474
+ const match = input.match(/\x1b\]1337;File=([^:]*):([^\x07]+)\x07/);
475
+ if (!match) return null;
476
+ try {
477
+ const args = match[1];
478
+ const base64Data = match[2];
479
+ const data = Buffer.from(base64Data, "base64");
480
+ let format = "png";
481
+ const nameMatch = args.match(/name=([^;]+)/);
482
+ if (nameMatch) {
483
+ const ext = extname2(nameMatch[1]).substring(1).toLowerCase();
484
+ if (ext && this.SUPPORTED_EXTENSIONS.includes(`.${ext}`)) {
485
+ format = ext === "jpeg" ? "jpg" : ext;
486
+ }
487
+ }
488
+ return {
489
+ type: "paste",
490
+ data,
491
+ format,
492
+ protocol: "iterm2"
493
+ };
494
+ } catch (error) {
495
+ return null;
496
+ }
497
+ }
498
+ /**
499
+ * Extract Kitty graphics protocol image
500
+ * Format: ESC_Ga=T,f=100;base64data ESC\
501
+ */
502
+ static extractKittyImage(input) {
503
+ const match = input.match(/\x1b_G([^;]*);([^\x1b]+)\x1b\\/);
504
+ if (!match) return null;
505
+ try {
506
+ const params = match[1];
507
+ const base64Data = match[2];
508
+ const data = Buffer.from(base64Data, "base64");
509
+ let format = "png";
510
+ const formatMatch = params.match(/f=(\d+)/);
511
+ if (formatMatch) {
512
+ const formatCode = parseInt(formatMatch[1], 10);
513
+ if (formatCode === 100) format = "jpg";
514
+ }
515
+ return {
516
+ type: "paste",
517
+ data,
518
+ format,
519
+ protocol: "kitty"
520
+ };
521
+ } catch (error) {
522
+ return null;
523
+ }
524
+ }
525
+ /**
526
+ * Extract Sixel image
527
+ * Format: ESC P q ... ESC \
528
+ */
529
+ static extractSixelImage(input) {
530
+ const match = input.match(/\x1bP([^q]*q[^\x1b]+)\x1b\\/);
531
+ if (!match) return null;
532
+ return null;
533
+ }
534
+ /**
535
+ * Extract base64 data URL
536
+ * Format: data:image/png;base64,iVBORw0KGgo...
537
+ */
538
+ static extractBase64Image(input) {
539
+ const match = input.match(/data:image\/(png|jpeg|jpg|gif|webp);base64,([A-Za-z0-9+/=]+)/);
540
+ if (!match) return null;
541
+ try {
542
+ const format = match[1] === "jpeg" ? "jpg" : match[1];
543
+ const base64Data = match[2];
544
+ const data = Buffer.from(base64Data, "base64");
545
+ return {
546
+ type: "paste",
547
+ data,
548
+ format,
549
+ protocol: "base64"
550
+ };
551
+ } catch (error) {
552
+ return null;
553
+ }
554
+ }
555
+ /**
556
+ * Extract dropped file path and read image
557
+ */
558
+ static extractDroppedFile(input) {
559
+ if (!this.looksLikeFilePath(input)) return null;
560
+ let filepath = input.trim();
561
+ filepath = filepath.replace(/^["']|["']$/g, "");
562
+ filepath = filepath.replace(/\\\s/g, " ");
563
+ if (!existsSync(filepath)) return null;
564
+ const stats = statSync2(filepath);
565
+ if (!stats.isFile()) return null;
566
+ const ext = extname2(filepath).toLowerCase();
567
+ if (!this.SUPPORTED_EXTENSIONS.includes(ext)) return null;
568
+ if (stats.size > this.MAX_IMAGE_SIZE) return null;
569
+ try {
570
+ const data = readFileSync(filepath);
571
+ const format = ext.substring(1) === "jpeg" ? "jpg" : ext.substring(1);
572
+ const filename = filepath.split("/").pop() || "image";
573
+ return {
574
+ type: "drop",
575
+ data,
576
+ format,
577
+ filename,
578
+ filepath
579
+ };
580
+ } catch (error) {
581
+ return null;
582
+ }
583
+ }
584
+ /**
585
+ * Check if string looks like a file path
586
+ */
587
+ static looksLikeFilePath(input) {
588
+ const trimmed = input.trim();
589
+ if (trimmed.startsWith("/") || trimmed.match(/^[a-zA-Z]:\\/)) {
590
+ return true;
591
+ }
592
+ if (trimmed.startsWith("~")) {
593
+ return true;
594
+ }
595
+ if (this.SUPPORTED_EXTENSIONS.some((ext) => trimmed.toLowerCase().endsWith(ext))) {
596
+ return true;
597
+ }
598
+ return false;
599
+ }
600
+ /**
601
+ * Remove image data from input string, leaving only text
602
+ */
603
+ static stripImageData(input) {
604
+ let cleaned = input;
605
+ cleaned = cleaned.replace(/\x1b\]1337;File=[^:]*:[^\x07]+\x07/g, "");
606
+ cleaned = cleaned.replace(/\x1b_G[^;]*;[^\x1b]+\x1b\\/g, "");
607
+ cleaned = cleaned.replace(/\x1bP[^q]*q[^\x1b]+\x1b\\/g, "");
608
+ cleaned = cleaned.replace(/data:image\/(png|jpeg|jpg|gif|webp);base64,[A-Za-z0-9+/=]+/g, "");
609
+ const filepath = this.extractDroppedFile(cleaned);
610
+ if (filepath) {
611
+ cleaned = cleaned.replace(filepath.filepath, "");
612
+ }
613
+ return cleaned.trim();
614
+ }
615
+ };
616
+
416
617
  // src/components/InputPrompt.tsx
417
618
  function findAtTrigger(value) {
418
619
  for (let i = value.length - 1; i >= 0; i--) {
@@ -429,12 +630,26 @@ function findAtTrigger(value) {
429
630
  }
430
631
  return null;
431
632
  }
432
- function InputPrompt({ onSubmit, disabled = false, history = [] }) {
633
+ function InputPrompt({
634
+ onSubmit,
635
+ onImageDetected,
636
+ disabled = false,
637
+ history = [],
638
+ prefillInput,
639
+ onPrefillConsumed
640
+ }) {
433
641
  const [value, setValue] = useState("");
434
642
  const [selectedIndex, setSelectedIndex] = useState(0);
435
643
  const [historyIndex, setHistoryIndex] = useState(-1);
436
644
  const [tempInput, setTempInput] = useState("");
437
645
  const [inputKey, setInputKey] = useState(0);
646
+ useEffect(() => {
647
+ if (prefillInput) {
648
+ setValue(prefillInput);
649
+ setInputKey((prev) => prev + 1);
650
+ onPrefillConsumed?.();
651
+ }
652
+ }, [prefillInput, onPrefillConsumed]);
438
653
  const [fileAutocomplete, setFileAutocomplete] = useState(null);
439
654
  const [fileSelectedIndex, setFileSelectedIndex] = useState(0);
440
655
  const shouldShowCommandAutocomplete = value.startsWith("/") && !disabled && !fileAutocomplete?.active;
@@ -550,7 +765,16 @@ function InputPrompt({ onSubmit, disabled = false, history = [] }) {
550
765
  setHistoryIndex(-1);
551
766
  setFileAutocomplete(null);
552
767
  };
553
- const handleChange = (newValue) => {
768
+ const handleChange = async (newValue) => {
769
+ if (ImageInputDetector.containsImageData(newValue)) {
770
+ const imageEvent = ImageInputDetector.extractImageData(newValue);
771
+ if (imageEvent && onImageDetected) {
772
+ const placeholder = await onImageDetected(imageEvent.data);
773
+ setValue(`${placeholder} `);
774
+ setInputKey((prev) => prev + 1);
775
+ return;
776
+ }
777
+ }
554
778
  setValue(newValue);
555
779
  if (historyIndex !== -1) {
556
780
  setHistoryIndex(-1);
@@ -1015,10 +1239,13 @@ function App({
1015
1239
  onMessage,
1016
1240
  onCommand,
1017
1241
  onPermissionResponse,
1242
+ onImageDetected,
1018
1243
  commandHistory = [],
1019
1244
  config,
1020
1245
  availableModels = [],
1021
- onSaveConfig
1246
+ onSaveConfig,
1247
+ prefillInput,
1248
+ onPrefillConsumed
1022
1249
  }) {
1023
1250
  const messages = useCliStore((state) => state.session?.messages || []);
1024
1251
  const sessionName = useCliStore((state) => state.session?.name || "New Session");
@@ -1034,7 +1261,7 @@ function App({
1034
1261
  async (input) => {
1035
1262
  const trimmed = input.trim();
1036
1263
  if (!trimmed) return;
1037
- if (trimmed.startsWith("/")) {
1264
+ if (trimmed.startsWith("/") && !trimmed.match(/^\/[A-Z]/)) {
1038
1265
  const [command, ...args] = trimmed.slice(1).split(" ");
1039
1266
  await onCommand(command, args);
1040
1267
  return;
@@ -1076,7 +1303,17 @@ ${errorBlock}`;
1076
1303
  canBeTrusted: permissionPrompt.canBeTrusted,
1077
1304
  onResponse: onPermissionResponse
1078
1305
  }
1079
- )) : /* @__PURE__ */ React9.createElement(React9.Fragment, null, /* @__PURE__ */ React9.createElement(Static, { items: messages }, (message, index) => /* @__PURE__ */ React9.createElement(Box9, { key: index, flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React9.createElement(MessageItem, { message }))), /* @__PURE__ */ React9.createElement(AgentThinking, null), exitRequested && /* @__PURE__ */ React9.createElement(Box9, { paddingX: 1, marginBottom: 1 }, /* @__PURE__ */ React9.createElement(Text8, { color: "yellow", bold: true }, "Press Ctrl+C again to exit")), /* @__PURE__ */ React9.createElement(Box9, { borderStyle: "single", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React9.createElement(InputPrompt, { onSubmit: handleSubmit, disabled: isThinking, history: commandHistory }))));
1306
+ )) : /* @__PURE__ */ React9.createElement(React9.Fragment, null, /* @__PURE__ */ React9.createElement(Static, { items: messages }, (message, index) => /* @__PURE__ */ React9.createElement(Box9, { key: index, flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React9.createElement(MessageItem, { message }))), /* @__PURE__ */ React9.createElement(AgentThinking, null), exitRequested && /* @__PURE__ */ React9.createElement(Box9, { paddingX: 1, marginBottom: 1 }, /* @__PURE__ */ React9.createElement(Text8, { color: "yellow", bold: true }, "Press Ctrl+C again to exit")), /* @__PURE__ */ React9.createElement(Box9, { borderStyle: "single", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React9.createElement(
1307
+ InputPrompt,
1308
+ {
1309
+ onSubmit: handleSubmit,
1310
+ onImageDetected,
1311
+ disabled: isThinking,
1312
+ history: commandHistory,
1313
+ prefillInput,
1314
+ onPrefillConsumed
1315
+ }
1316
+ ))));
1080
1317
  }
1081
1318
  function MessageItem({ message }) {
1082
1319
  return /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React9.createElement(Box9, null, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: message.role === "user" ? "cyan" : "green" }, message.role === "user" ? "\u{1F464} You" : "\u{1F916} Assistant"), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \u2022 ", new Date(message.timestamp).toLocaleTimeString())), /* @__PURE__ */ React9.createElement(Box9, { paddingLeft: 2 }, message.metadata?.permissionDenied ? /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, "\u26A0\uFE0F ", message.content) : /* @__PURE__ */ React9.createElement(Text8, null, message.content)), message.metadata?.steps && message.metadata.steps.length > 0 && /* @__PURE__ */ React9.createElement(Box9, { paddingLeft: 2, marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true, bold: true }, "\u{1F527} Agent Reasoning Trace (", message.metadata.steps.filter((s) => s.type === "action").length, " ", "tools used, ", message.metadata.steps.length, " total steps)", message.metadata.tokenUsage && ` \u2022 ${message.metadata.tokenUsage.total} tokens`), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "Step types: ", message.metadata.steps.map((s) => s.type).join(", ")), message.metadata.steps.map((step, idx) => {
@@ -1166,9 +1403,78 @@ function TrustLocationSelector({ inProject, onSelect, onCancel }) {
1166
1403
  ), /* @__PURE__ */ React11.createElement(Box11, { marginTop: 1 }, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "Use \u2191\u2193 arrows to navigate, Enter to select, Ctrl+C to cancel")));
1167
1404
  }
1168
1405
 
1406
+ // src/components/RewindSelector.tsx
1407
+ import React12, { useState as useState3 } from "react";
1408
+ import { Box as Box12, Text as Text11, useInput as useInput3 } from "ink";
1409
+ import SelectInput3 from "ink-select-input";
1410
+ function RewindSelector({ messages, onSelect, onCancel }) {
1411
+ const [step, setStep] = useState3("selection");
1412
+ const [selectedMessageIndex, setSelectedMessageIndex] = useState3(null);
1413
+ useInput3((input, key) => {
1414
+ if (key.escape) {
1415
+ if (step === "confirmation") {
1416
+ setStep("selection");
1417
+ setSelectedMessageIndex(null);
1418
+ } else {
1419
+ onCancel();
1420
+ }
1421
+ }
1422
+ });
1423
+ const userMessages = messages.map((msg, idx) => ({ msg, originalIndex: idx })).filter(({ msg }) => msg.role === "user");
1424
+ const items = userMessages.map(({ msg, originalIndex }, index) => {
1425
+ const preview = msg.content.length > 50 ? msg.content.substring(0, 50) + "..." : msg.content;
1426
+ const timestamp = new Date(msg.timestamp).toLocaleString();
1427
+ const position = index + 1;
1428
+ return {
1429
+ label: `[${position}] ${timestamp} - ${preview}`,
1430
+ value: originalIndex
1431
+ };
1432
+ });
1433
+ const handleSelectionSelect = (item) => {
1434
+ setSelectedMessageIndex(item.value);
1435
+ setStep("confirmation");
1436
+ };
1437
+ const handleConfirmationSelect = (item) => {
1438
+ if (item.value === "confirm" && selectedMessageIndex !== null) {
1439
+ onSelect(selectedMessageIndex);
1440
+ } else {
1441
+ setStep("selection");
1442
+ setSelectedMessageIndex(null);
1443
+ }
1444
+ };
1445
+ const messagesToRemove = selectedMessageIndex !== null && selectedMessageIndex >= 0 && selectedMessageIndex < messages.length ? messages.slice(selectedMessageIndex) : [];
1446
+ const tokensToRemove = messagesToRemove.reduce((sum2, msg) => {
1447
+ return sum2 + (msg.metadata?.tokenUsage?.total || 0);
1448
+ }, 0);
1449
+ if (step === "selection") {
1450
+ return /* @__PURE__ */ React12.createElement(Box12, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ React12.createElement(Box12, { marginBottom: 1 }, /* @__PURE__ */ React12.createElement(Text11, { bold: true }, "Select a point to rewind to:")), /* @__PURE__ */ React12.createElement(
1451
+ SelectInput3,
1452
+ {
1453
+ items,
1454
+ onSelect: handleSelectionSelect,
1455
+ itemComponent: ({ isSelected, label }) => /* @__PURE__ */ React12.createElement(Box12, null, /* @__PURE__ */ React12.createElement(Text11, { color: isSelected ? "cyan" : void 0 }, isSelected ? "\u276F " : " ", label))
1456
+ }
1457
+ ), /* @__PURE__ */ React12.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, "Use \u2191\u2193 arrows to navigate, Enter to select, Esc to cancel")));
1458
+ }
1459
+ const confirmationItems = [
1460
+ { label: "Yes, rewind to this point", value: "confirm" },
1461
+ { label: "No, go back to selection", value: "cancel" }
1462
+ ];
1463
+ const selectedMessage = selectedMessageIndex !== null && selectedMessageIndex >= 0 && selectedMessageIndex < messages.length ? messages[selectedMessageIndex] : null;
1464
+ const selectedPreview = selectedMessage ? selectedMessage.content.length > 50 ? selectedMessage.content.substring(0, 50) + "..." : selectedMessage.content : "";
1465
+ return /* @__PURE__ */ React12.createElement(Box12, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ React12.createElement(Box12, { marginBottom: 1, flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Text11, { bold: true }, "Confirm rewind operation:"), /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, "Message: ", selectedPreview), /* @__PURE__ */ React12.createElement(Text11, { color: "yellow" }, "This will remove ", messagesToRemove.length, " message(s) (", tokensToRemove.toLocaleString(), " tokens)"), /* @__PURE__ */ React12.createElement(Text11, { color: "cyan" }, "The selected message will be placed in your input for editing.")), /* @__PURE__ */ React12.createElement(
1466
+ SelectInput3,
1467
+ {
1468
+ items: confirmationItems,
1469
+ onSelect: handleConfirmationSelect,
1470
+ itemComponent: ({ isSelected, label }) => /* @__PURE__ */ React12.createElement(Box12, null, /* @__PURE__ */ React12.createElement(Text11, { color: isSelected ? "cyan" : void 0 }, isSelected ? "\u276F " : " ", label))
1471
+ }
1472
+ ), /* @__PURE__ */ React12.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, "Use \u2191\u2193 arrows to navigate, Enter to select, Esc to go back")));
1473
+ }
1474
+
1169
1475
  // src/components/LoginFlow.tsx
1170
- import React12, { useState as useState3, useEffect as useEffect2 } from "react";
1171
- import { Box as Box12, Text as Text11 } from "ink";
1476
+ import React13, { useState as useState4, useEffect as useEffect2 } from "react";
1477
+ import { Box as Box13, Text as Text12 } from "ink";
1172
1478
  import Spinner2 from "ink-spinner";
1173
1479
  import jwt from "jsonwebtoken";
1174
1480
  import open from "open";
@@ -1284,10 +1590,10 @@ var OAuthClient = class {
1284
1590
 
1285
1591
  // src/components/LoginFlow.tsx
1286
1592
  function LoginFlow({ apiUrl = "http://localhost:3000", configStore, onSuccess, onError }) {
1287
- const [status, setStatus] = useState3("initiating");
1288
- const [deviceFlow, setDeviceFlow] = useState3(null);
1289
- const [statusMessage, setStatusMessage] = useState3("Initiating device authorization...");
1290
- const [error, setError] = useState3(null);
1593
+ const [status, setStatus] = useState4("initiating");
1594
+ const [deviceFlow, setDeviceFlow] = useState4(null);
1595
+ const [statusMessage, setStatusMessage] = useState4("Initiating device authorization...");
1596
+ const [error, setError] = useState4(null);
1291
1597
  useEffect2(() => {
1292
1598
  const runLoginFlow = async () => {
1293
1599
  const oauth = new OAuthClient(apiUrl);
@@ -1331,15 +1637,15 @@ function LoginFlow({ apiUrl = "http://localhost:3000", configStore, onSuccess, o
1331
1637
  }
1332
1638
  }, [deviceFlow, status]);
1333
1639
  if (status === "initiating") {
1334
- return /* @__PURE__ */ React12.createElement(Box12, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React12.createElement(Box12, null, /* @__PURE__ */ React12.createElement(Text11, { color: "cyan" }, /* @__PURE__ */ React12.createElement(Spinner2, { type: "dots" }), " Initiating device authorization...")));
1640
+ return /* @__PURE__ */ React13.createElement(Box13, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React13.createElement(Box13, null, /* @__PURE__ */ React13.createElement(Text12, { color: "cyan" }, /* @__PURE__ */ React13.createElement(Spinner2, { type: "dots" }), " Initiating device authorization...")));
1335
1641
  }
1336
1642
  if (status === "error") {
1337
- return /* @__PURE__ */ React12.createElement(Box12, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React12.createElement(Box12, { marginBottom: 1 }, /* @__PURE__ */ React12.createElement(Text11, { color: "red", bold: true }, "\u2716 Authentication Failed")), /* @__PURE__ */ React12.createElement(Box12, null, /* @__PURE__ */ React12.createElement(Text11, { color: "red" }, error)));
1643
+ return /* @__PURE__ */ React13.createElement(Box13, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React13.createElement(Box13, { marginBottom: 1 }, /* @__PURE__ */ React13.createElement(Text12, { color: "red", bold: true }, "\u2716 Authentication Failed")), /* @__PURE__ */ React13.createElement(Box13, null, /* @__PURE__ */ React13.createElement(Text12, { color: "red" }, error)));
1338
1644
  }
1339
1645
  if (status === "success") {
1340
- return /* @__PURE__ */ React12.createElement(Box12, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React12.createElement(Box12, { marginBottom: 1 }, /* @__PURE__ */ React12.createElement(Text11, { color: "green", bold: true }, "\u2714 Successfully authenticated!")), /* @__PURE__ */ React12.createElement(Box12, null, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, "You can now use B4M CLI with your account.")));
1646
+ return /* @__PURE__ */ React13.createElement(Box13, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React13.createElement(Box13, { marginBottom: 1 }, /* @__PURE__ */ React13.createElement(Text12, { color: "green", bold: true }, "\u2714 Successfully authenticated!")), /* @__PURE__ */ React13.createElement(Box13, null, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, "You can now use B4M CLI with your account.")));
1341
1647
  }
1342
- return /* @__PURE__ */ React12.createElement(Box12, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan" }, /* @__PURE__ */ React12.createElement(Box12, { marginBottom: 1 }, /* @__PURE__ */ React12.createElement(Text11, { color: "cyan", bold: true }, "\u{1F510} Device Authorization")), /* @__PURE__ */ React12.createElement(Box12, { marginBottom: 1 }, /* @__PURE__ */ React12.createElement(Text11, null, "Opening browser automatically... If it doesn't open, please visit:")), /* @__PURE__ */ React12.createElement(Box12, { marginBottom: 1, paddingLeft: 2 }, /* @__PURE__ */ React12.createElement(Text11, { color: "blue", bold: true }, deviceFlow?.verification_uri)), /* @__PURE__ */ React12.createElement(Box12, { marginBottom: 1 }, /* @__PURE__ */ React12.createElement(Text11, null, "And enter this code when prompted:")), /* @__PURE__ */ React12.createElement(Box12, { marginBottom: 1, paddingLeft: 2 }, /* @__PURE__ */ React12.createElement(Text11, { color: "yellow", bold: true }, deviceFlow?.user_code)), /* @__PURE__ */ React12.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React12.createElement(Text11, { color: "cyan" }, /* @__PURE__ */ React12.createElement(Spinner2, { type: "dots" }), " ", statusMessage)), /* @__PURE__ */ React12.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, "Expires in ", deviceFlow ? Math.floor(deviceFlow.expires_in / 60) : 0, " minutes")));
1648
+ return /* @__PURE__ */ React13.createElement(Box13, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan" }, /* @__PURE__ */ React13.createElement(Box13, { marginBottom: 1 }, /* @__PURE__ */ React13.createElement(Text12, { color: "cyan", bold: true }, "\u{1F510} Device Authorization")), /* @__PURE__ */ React13.createElement(Box13, { marginBottom: 1 }, /* @__PURE__ */ React13.createElement(Text12, null, "Opening browser automatically... If it doesn't open, please visit:")), /* @__PURE__ */ React13.createElement(Box13, { marginBottom: 1, paddingLeft: 2 }, /* @__PURE__ */ React13.createElement(Text12, { color: "blue", bold: true }, deviceFlow?.verification_uri)), /* @__PURE__ */ React13.createElement(Box13, { marginBottom: 1 }, /* @__PURE__ */ React13.createElement(Text12, null, "And enter this code when prompted:")), /* @__PURE__ */ React13.createElement(Box13, { marginBottom: 1, paddingLeft: 2 }, /* @__PURE__ */ React13.createElement(Text12, { color: "yellow", bold: true }, deviceFlow?.user_code)), /* @__PURE__ */ React13.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React13.createElement(Text12, { color: "cyan" }, /* @__PURE__ */ React13.createElement(Spinner2, { type: "dots" }), " ", statusMessage)), /* @__PURE__ */ React13.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, "Expires in ", deviceFlow ? Math.floor(deviceFlow.expires_in / 60) : 0, " minutes")));
1343
1649
  }
1344
1650
 
1345
1651
  // src/storage/SessionStore.ts
@@ -1461,7 +1767,7 @@ var SessionStore = class {
1461
1767
  };
1462
1768
 
1463
1769
  // src/storage/ConfigStore.ts
1464
- import { promises as fs4, existsSync as existsSync2 } from "fs";
1770
+ import { promises as fs4, existsSync as existsSync3 } from "fs";
1465
1771
  import path4 from "path";
1466
1772
  import { homedir as homedir2 } from "os";
1467
1773
  import { v4 as uuidv4 } from "uuid";
@@ -1588,7 +1894,7 @@ function findProjectConfigDir(startDir = process.cwd()) {
1588
1894
  while (currentDir !== root) {
1589
1895
  const gitPath = path4.join(currentDir, ".git");
1590
1896
  try {
1591
- if (existsSync2(gitPath)) {
1897
+ if (existsSync3(gitPath)) {
1592
1898
  return currentDir;
1593
1899
  }
1594
1900
  } catch {
@@ -2196,7 +2502,7 @@ var ReActAgent = class extends EventEmitter {
2196
2502
  /**
2197
2503
  * Run the agent to completion
2198
2504
  *
2199
- * @param query - The user's question or task
2505
+ * @param query - The user's question or task (can be text or multimodal content with images)
2200
2506
  * @param options - Optional overrides for this run
2201
2507
  * @returns Agent result with final answer and all steps
2202
2508
  */
@@ -7673,7 +7979,7 @@ var planetVisibilityTool = {
7673
7979
 
7674
7980
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/fileRead/index.js
7675
7981
  import { promises as fs7 } from "fs";
7676
- import { existsSync as existsSync3, statSync as statSync3 } from "fs";
7982
+ import { existsSync as existsSync4, statSync as statSync4 } from "fs";
7677
7983
  import path6 from "path";
7678
7984
  var MAX_FILE_SIZE2 = 10 * 1024 * 1024;
7679
7985
  async function readFileContent(params) {
@@ -7684,10 +7990,10 @@ async function readFileContent(params) {
7684
7990
  if (!resolvedPath.startsWith(cwd)) {
7685
7991
  throw new Error(`Access denied: Cannot read files outside of current working directory`);
7686
7992
  }
7687
- if (!existsSync3(resolvedPath)) {
7993
+ if (!existsSync4(resolvedPath)) {
7688
7994
  throw new Error(`File not found: ${filePath}`);
7689
7995
  }
7690
- const stats = statSync3(resolvedPath);
7996
+ const stats = statSync4(resolvedPath);
7691
7997
  if (stats.isDirectory()) {
7692
7998
  throw new Error(`Path is a directory, not a file: ${filePath}`);
7693
7999
  }
@@ -7730,7 +8036,7 @@ var fileReadTool = {
7730
8036
  context.logger.info("\u{1F4C4} FileRead: Reading file", { path: params.path });
7731
8037
  try {
7732
8038
  const content = await readFileContent(params);
7733
- const stats = statSync3(path6.resolve(process.cwd(), path6.normalize(params.path)));
8039
+ const stats = statSync4(path6.resolve(process.cwd(), path6.normalize(params.path)));
7734
8040
  context.logger.info("\u2705 FileRead: Success", {
7735
8041
  path: params.path,
7736
8042
  size: stats.size,
@@ -7770,7 +8076,7 @@ var fileReadTool = {
7770
8076
 
7771
8077
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/createFile/index.js
7772
8078
  import { promises as fs8 } from "fs";
7773
- import { existsSync as existsSync4 } from "fs";
8079
+ import { existsSync as existsSync5 } from "fs";
7774
8080
  import path7 from "path";
7775
8081
  async function createFile(params) {
7776
8082
  const { path: filePath, content, createDirectories = true } = params;
@@ -7780,7 +8086,7 @@ async function createFile(params) {
7780
8086
  if (!resolvedPath.startsWith(cwd)) {
7781
8087
  throw new Error(`Access denied: Cannot create files outside of current working directory`);
7782
8088
  }
7783
- const fileExists = existsSync4(resolvedPath);
8089
+ const fileExists = existsSync5(resolvedPath);
7784
8090
  const action = fileExists ? "overwritten" : "created";
7785
8091
  if (createDirectories) {
7786
8092
  const dir = path7.dirname(resolvedPath);
@@ -7798,7 +8104,7 @@ var createFileTool = {
7798
8104
  implementation: (context) => ({
7799
8105
  toolFn: async (value) => {
7800
8106
  const params = value;
7801
- const fileExists = existsSync4(path7.resolve(process.cwd(), path7.normalize(params.path)));
8107
+ const fileExists = existsSync5(path7.resolve(process.cwd(), path7.normalize(params.path)));
7802
8108
  context.logger.info(`\u{1F4DD} CreateFile: ${fileExists ? "Overwriting" : "Creating"} file`, {
7803
8109
  path: params.path,
7804
8110
  size: params.content.length
@@ -7839,7 +8145,7 @@ var createFileTool = {
7839
8145
 
7840
8146
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/globFiles/index.js
7841
8147
  import { readdir } from "fs/promises";
7842
- import { statSync as statSync4 } from "fs";
8148
+ import { statSync as statSync5 } from "fs";
7843
8149
  import path8 from "path";
7844
8150
  function matchGlob(filePath, pattern) {
7845
8151
  let regexPattern = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/\{([^}]+)\}/g, (_, options) => `(${options.split(",").join("|")})`).replace(/\[([^\]]+)\]/g, "[$1]");
@@ -7887,7 +8193,7 @@ async function listFiles(params) {
7887
8193
  for (const match of matches) {
7888
8194
  try {
7889
8195
  const fullPath = path8.join(targetCwd, match);
7890
- const stats = statSync4(fullPath);
8196
+ const stats = statSync5(fullPath);
7891
8197
  filesInfo.push({
7892
8198
  path: match,
7893
8199
  size: stats.size,
@@ -7968,7 +8274,7 @@ var globFilesTool = {
7968
8274
 
7969
8275
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/grepSearch/index.js
7970
8276
  import { readdir as readdir2 } from "fs/promises";
7971
- import { readFileSync as readFileSync2, statSync as statSync5 } from "fs";
8277
+ import { readFileSync as readFileSync3, statSync as statSync6 } from "fs";
7972
8278
  import path9 from "path";
7973
8279
  var MAX_FILE_SIZE3 = 10 * 1024 * 1024;
7974
8280
  function matchGlob2(filePath, pattern) {
@@ -8020,7 +8326,7 @@ async function searchFiles2(params) {
8020
8326
  for (const match of filesToSearch) {
8021
8327
  const fullPath = path9.join(targetCwd, match);
8022
8328
  try {
8023
- const stats = statSync5(fullPath);
8329
+ const stats = statSync6(fullPath);
8024
8330
  if (stats.isDirectory() || stats.size > MAX_FILE_SIZE3) {
8025
8331
  continue;
8026
8332
  }
@@ -8044,7 +8350,7 @@ async function searchFiles2(params) {
8044
8350
  filesSearched++;
8045
8351
  const fullPath = path9.join(targetCwd, file);
8046
8352
  try {
8047
- const content = readFileSync2(fullPath, "utf-8");
8353
+ const content = readFileSync3(fullPath, "utf-8");
8048
8354
  const lines = content.split("\n");
8049
8355
  for (let i = 0; i < lines.length; i++) {
8050
8356
  if (matches.length >= maxResults) {
@@ -8178,7 +8484,7 @@ var grepSearchTool = {
8178
8484
 
8179
8485
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/deleteFile/index.js
8180
8486
  import { promises as fs9 } from "fs";
8181
- import { existsSync as existsSync5, statSync as statSync6 } from "fs";
8487
+ import { existsSync as existsSync6, statSync as statSync7 } from "fs";
8182
8488
  import path10 from "path";
8183
8489
  async function deleteFile(params) {
8184
8490
  const { path: filePath, recursive = false } = params;
@@ -8188,10 +8494,10 @@ async function deleteFile(params) {
8188
8494
  if (!resolvedPath.startsWith(cwd)) {
8189
8495
  throw new Error(`Access denied: Cannot delete files outside of current working directory`);
8190
8496
  }
8191
- if (!existsSync5(resolvedPath)) {
8497
+ if (!existsSync6(resolvedPath)) {
8192
8498
  throw new Error(`File or directory not found: ${filePath}`);
8193
8499
  }
8194
- const stats = statSync6(resolvedPath);
8500
+ const stats = statSync7(resolvedPath);
8195
8501
  const isDirectory = stats.isDirectory();
8196
8502
  const size = stats.size;
8197
8503
  if (isDirectory && !recursive) {
@@ -8212,7 +8518,7 @@ var deleteFileTool = {
8212
8518
  toolFn: async (value) => {
8213
8519
  const params = value;
8214
8520
  const resolvedPath = path10.resolve(process.cwd(), path10.normalize(params.path));
8215
- const isDirectory = existsSync5(resolvedPath) && statSync6(resolvedPath).isDirectory();
8521
+ const isDirectory = existsSync6(resolvedPath) && statSync7(resolvedPath).isDirectory();
8216
8522
  context.logger.info(`\u{1F5D1}\uFE0F DeleteFile: Deleting ${isDirectory ? "directory" : "file"}`, {
8217
8523
  path: params.path,
8218
8524
  recursive: params.recursive
@@ -8502,10 +8808,10 @@ import jwt2 from "jsonwebtoken";
8502
8808
  // src/utils/diffPreview.ts
8503
8809
  import * as Diff from "diff";
8504
8810
  import { readFile } from "fs/promises";
8505
- import { existsSync as existsSync6 } from "fs";
8811
+ import { existsSync as existsSync7 } from "fs";
8506
8812
  async function generateFileDiffPreview(args) {
8507
8813
  try {
8508
- if (!existsSync6(args.path)) {
8814
+ if (!existsSync7(args.path)) {
8509
8815
  const lines2 = args.content.split("\n");
8510
8816
  const preview = lines2.slice(0, 20).join("\n");
8511
8817
  const hasMore = lines2.length > 20;
@@ -8535,7 +8841,7 @@ ${preview}${hasMore ? `
8535
8841
  }
8536
8842
  async function generateFileDeletePreview(args) {
8537
8843
  try {
8538
- if (!existsSync6(args.path)) {
8844
+ if (!existsSync7(args.path)) {
8539
8845
  return `[File does not exist: ${args.path}]`;
8540
8846
  }
8541
8847
  const stats = await import("fs/promises").then((fs11) => fs11.stat(args.path));
@@ -9197,7 +9503,7 @@ var logger = Logger2.getInstance();
9197
9503
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
9198
9504
  import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
9199
9505
  import path12 from "path";
9200
- import { existsSync as existsSync7, readdirSync as readdirSync3 } from "fs";
9506
+ import { existsSync as existsSync8, readdirSync as readdirSync3 } from "fs";
9201
9507
  var MCPClient = class {
9202
9508
  // Note: This class handles MCP server communication with repository filtering
9203
9509
  mcp;
@@ -9248,7 +9554,7 @@ var MCPClient = class {
9248
9554
  path12.join(root, `/b4m-core/packages/mcp/dist/src/${this.serverName}/index.js`),
9249
9555
  path12.join(root, "core", "mcp", "servers", this.serverName, "dist", "index.js")
9250
9556
  ];
9251
- const serverScriptPath = candidatePaths.find((p) => existsSync7(p));
9557
+ const serverScriptPath = candidatePaths.find((p) => existsSync8(p));
9252
9558
  if (!serverScriptPath) {
9253
9559
  const getDirectories = (source) => readdirSync3(source, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
9254
9560
  console.error(`[MCP] Server script not found. Tried paths:`, candidatePaths);
@@ -9446,6 +9752,190 @@ var McpManager = class {
9446
9752
  }
9447
9753
  };
9448
9754
 
9755
+ // src/utils/imageRenderer.ts
9756
+ var ImageRenderer = class {
9757
+ constructor() {
9758
+ this.imageCounter = 0;
9759
+ this.placeholderMap = /* @__PURE__ */ new Map();
9760
+ }
9761
+ /**
9762
+ * Generate placeholder for a new image
9763
+ */
9764
+ createPlaceholder(hash) {
9765
+ const existing = this.placeholderMap.get(hash);
9766
+ if (existing) {
9767
+ return existing.placeholder;
9768
+ }
9769
+ this.imageCounter++;
9770
+ const placeholder = `[Image ${this.imageCounter}]`;
9771
+ this.placeholderMap.set(hash, {
9772
+ hash,
9773
+ number: this.imageCounter,
9774
+ placeholder
9775
+ });
9776
+ return placeholder;
9777
+ }
9778
+ /**
9779
+ * Get all image hashes from a message containing placeholders
9780
+ */
9781
+ extractImageHashes(message) {
9782
+ const hashes = [];
9783
+ const placeholderRegex = /\[Image \d+\]/g;
9784
+ const matches = message.match(placeholderRegex);
9785
+ if (!matches) return hashes;
9786
+ for (const [hash, data] of this.placeholderMap.entries()) {
9787
+ if (matches.includes(data.placeholder)) {
9788
+ hashes.push(hash);
9789
+ }
9790
+ }
9791
+ return hashes;
9792
+ }
9793
+ /**
9794
+ * Get hash by placeholder text
9795
+ */
9796
+ getHashFromPlaceholder(placeholder) {
9797
+ for (const [hash, data] of this.placeholderMap.entries()) {
9798
+ if (data.placeholder === placeholder) {
9799
+ return hash;
9800
+ }
9801
+ }
9802
+ return null;
9803
+ }
9804
+ };
9805
+
9806
+ // src/utils/messageBuilder.ts
9807
+ var MessageBuilder = class {
9808
+ constructor(imageStore, imageRenderer) {
9809
+ this.imageStore = imageStore;
9810
+ this.imageRenderer = imageRenderer;
9811
+ }
9812
+ /**
9813
+ * Build a multimodal message from user input
9814
+ * Extracts image placeholders and creates proper message structure with base64-encoded images
9815
+ */
9816
+ async buildMessage(userInput, role = "user") {
9817
+ const imageHashes = this.imageRenderer.extractImageHashes(userInput);
9818
+ if (imageHashes.length === 0) {
9819
+ return {
9820
+ message: {
9821
+ role,
9822
+ content: userInput
9823
+ },
9824
+ uploadedImages: []
9825
+ };
9826
+ }
9827
+ const content = [];
9828
+ const parts = this.splitMessageByPlaceholders(userInput);
9829
+ for (const part of parts) {
9830
+ if (part.type === "text" && part.content.trim()) {
9831
+ content.push({
9832
+ type: "text",
9833
+ text: part.content
9834
+ });
9835
+ } else if (part.type === "image") {
9836
+ const hash = this.imageRenderer.getHashFromPlaceholder(part.content);
9837
+ if (hash) {
9838
+ const imageData = this.imageStore.readImage(hash);
9839
+ const metadata = this.imageStore.getMetadata(hash);
9840
+ if (imageData && metadata) {
9841
+ const base64Data = imageData.toString("base64");
9842
+ const mediaType = this.getMediaType(metadata.format);
9843
+ content.push({
9844
+ type: "image",
9845
+ source: {
9846
+ type: "base64",
9847
+ media_type: mediaType,
9848
+ data: base64Data
9849
+ }
9850
+ });
9851
+ }
9852
+ }
9853
+ }
9854
+ }
9855
+ return {
9856
+ message: {
9857
+ role,
9858
+ content
9859
+ },
9860
+ uploadedImages: []
9861
+ };
9862
+ }
9863
+ /**
9864
+ * Get MIME type for image format
9865
+ */
9866
+ getMediaType(format) {
9867
+ switch (format.toLowerCase()) {
9868
+ case "png":
9869
+ return "image/png";
9870
+ case "jpg":
9871
+ case "jpeg":
9872
+ return "image/jpeg";
9873
+ case "gif":
9874
+ return "image/gif";
9875
+ case "webp":
9876
+ return "image/webp";
9877
+ default:
9878
+ return "image/png";
9879
+ }
9880
+ }
9881
+ /**
9882
+ * Split message text by image placeholders
9883
+ * Returns array of text and image parts in order
9884
+ */
9885
+ splitMessageByPlaceholders(message) {
9886
+ const parts = [];
9887
+ const placeholderRegex = /(\[Image \d+\])/g;
9888
+ let lastIndex = 0;
9889
+ let match;
9890
+ while ((match = placeholderRegex.exec(message)) !== null) {
9891
+ if (match.index > lastIndex) {
9892
+ const text = message.substring(lastIndex, match.index);
9893
+ if (text) {
9894
+ parts.push({ type: "text", content: text });
9895
+ }
9896
+ }
9897
+ parts.push({ type: "image", content: match[0] });
9898
+ lastIndex = match.index + match[0].length;
9899
+ }
9900
+ if (lastIndex < message.length) {
9901
+ const text = message.substring(lastIndex);
9902
+ if (text) {
9903
+ parts.push({ type: "text", content: text });
9904
+ }
9905
+ }
9906
+ if (parts.length === 0) {
9907
+ parts.push({ type: "text", content: message });
9908
+ }
9909
+ return parts;
9910
+ }
9911
+ /**
9912
+ * Check if a message contains images
9913
+ */
9914
+ hasImages(message) {
9915
+ return this.imageRenderer.extractImageHashes(message).length > 0;
9916
+ }
9917
+ /**
9918
+ * Convert multimodal message back to simple string (for display/logging)
9919
+ */
9920
+ messageToString(message) {
9921
+ if (typeof message.content === "string") {
9922
+ return message.content;
9923
+ }
9924
+ return message.content.map((item) => {
9925
+ if (item.type === "text") {
9926
+ return item.text;
9927
+ } else if (item.type === "image") {
9928
+ if (item.source.type === "url") {
9929
+ return `[Image: ${item.source.url}]`;
9930
+ } else {
9931
+ return `[Image: base64 ${item.source.media_type}]`;
9932
+ }
9933
+ }
9934
+ return "";
9935
+ }).join("");
9936
+ }
9937
+ };
9938
+
9449
9939
  // src/llm/ServerLlmBackend.ts
9450
9940
  import { createParser } from "eventsource-parser";
9451
9941
  import { isAxiosError } from "axios";
@@ -9863,7 +10353,7 @@ import { isAxiosError as isAxiosError2 } from "axios";
9863
10353
  // package.json
9864
10354
  var package_default = {
9865
10355
  name: "@bike4mind/cli",
9866
- version: "0.2.6-fix-cli-mcp-type-mismatch.17119+0906e1e34",
10356
+ version: "0.2.7-feat-cli-rewind-command.17128+8d4de110f",
9867
10357
  type: "module",
9868
10358
  description: "Interactive CLI tool for Bike4Mind with ReAct agents",
9869
10359
  license: "UNLICENSED",
@@ -9924,6 +10414,7 @@ var package_default = {
9924
10414
  "async-mutex": "^0.5.0",
9925
10415
  axios: "^1.13.2",
9926
10416
  bcryptjs: "^2.4.3",
10417
+ "better-sqlite3": "^12.5.0",
9927
10418
  cheerio: "1.0.0-rc.12",
9928
10419
  "csv-parse": "^5.5.6",
9929
10420
  dayjs: "^1.11.13",
@@ -9949,7 +10440,7 @@ var package_default = {
9949
10440
  "p-limit": "^6.2.0",
9950
10441
  qrcode: "^1.5.4",
9951
10442
  react: "^19.2.3",
9952
- sharp: "^0.32.6",
10443
+ sharp: "^0.34.5",
9953
10444
  speakeasy: "^2.0.0",
9954
10445
  tiktoken: "^1.0.16",
9955
10446
  turndown: "^7.2.0",
@@ -9965,10 +10456,11 @@ var package_default = {
9965
10456
  },
9966
10457
  devDependencies: {
9967
10458
  "@bike4mind/agents": "0.1.0",
9968
- "@bike4mind/common": "2.38.1-fix-cli-mcp-type-mismatch.17119+0906e1e34",
9969
- "@bike4mind/mcp": "1.20.1-fix-cli-mcp-type-mismatch.17119+0906e1e34",
9970
- "@bike4mind/services": "2.33.1-fix-cli-mcp-type-mismatch.17119+0906e1e34",
9971
- "@bike4mind/utils": "2.1.2-fix-cli-mcp-type-mismatch.17119+0906e1e34",
10459
+ "@bike4mind/common": "2.38.1-feat-cli-rewind-command.17128+8d4de110f",
10460
+ "@bike4mind/mcp": "1.20.1-feat-cli-rewind-command.17128+8d4de110f",
10461
+ "@bike4mind/services": "2.33.1-feat-cli-rewind-command.17128+8d4de110f",
10462
+ "@bike4mind/utils": "2.1.2-feat-cli-rewind-command.17128+8d4de110f",
10463
+ "@types/better-sqlite3": "^7.6.12",
9972
10464
  "@types/diff": "^5.0.9",
9973
10465
  "@types/jsonwebtoken": "^9.0.4",
9974
10466
  "@types/node": "^22.9.0",
@@ -9980,7 +10472,7 @@ var package_default = {
9980
10472
  typescript: "^5.9.3",
9981
10473
  vitest: "^3.2.4"
9982
10474
  },
9983
- gitHead: "0906e1e34709a52074b386d9b51a94776db1d33d"
10475
+ gitHead: "8d4de110f7d8f55bff6bd26cb38b8ca1d4dbd996"
9984
10476
  };
9985
10477
 
9986
10478
  // src/config/constants.ts
@@ -10001,20 +10493,27 @@ var EXIT_TIMEOUT_MS = 2e3;
10001
10493
  var usageCache = null;
10002
10494
  function CliApp() {
10003
10495
  const { exit } = useApp();
10004
- const [state, setState] = useState4({
10496
+ const imageRenderer = new ImageRenderer();
10497
+ const [state, setState] = useState5({
10005
10498
  session: null,
10006
10499
  sessionStore: new SessionStore(),
10007
10500
  configStore: new ConfigStore(),
10008
10501
  commandHistoryStore: new CommandHistoryStore(),
10502
+ imageStore: null,
10503
+ // Lazy-loaded on first use
10504
+ imageRenderer,
10505
+ messageBuilder: null,
10009
10506
  agent: null,
10010
10507
  mcpManager: null,
10011
10508
  permissionManager: null,
10012
10509
  permissionPrompt: null,
10013
- trustLocationSelector: null
10510
+ trustLocationSelector: null,
10511
+ rewindSelector: null
10014
10512
  });
10015
- const [isInitialized, setIsInitialized] = useState4(false);
10016
- const [initError, setInitError] = useState4(null);
10017
- const [commandHistory, setCommandHistory] = useState4([]);
10513
+ const [isInitialized, setIsInitialized] = useState5(false);
10514
+ const [initError, setInitError] = useState5(null);
10515
+ const [commandHistory, setCommandHistory] = useState5([]);
10516
+ const imageStoreInitPromise = useRef(null);
10018
10517
  const setStoreSession = useCliStore((state2) => state2.setSession);
10019
10518
  const setPermissionPrompt = useCliStore((state2) => state2.setPermissionPrompt);
10020
10519
  const setShowConfigEditor = useCliStore((state2) => state2.setShowConfigEditor);
@@ -10038,6 +10537,13 @@ function CliApp() {
10038
10537
  if (state.agent) {
10039
10538
  state.agent.removeAllListeners();
10040
10539
  }
10540
+ if (state.imageStore) {
10541
+ try {
10542
+ state.imageStore.close();
10543
+ } catch (err) {
10544
+ logger.debug(`[CLEANUP] Image store close error: ${err}`);
10545
+ }
10546
+ }
10041
10547
  await Promise.race([
10042
10548
  Promise.all(cleanupTasks),
10043
10549
  new Promise((resolve3) => setTimeout(resolve3, 500))
@@ -10046,8 +10552,8 @@ function CliApp() {
10046
10552
  setTimeout(() => {
10047
10553
  process.exit(0);
10048
10554
  }, 100);
10049
- }, [state.session, state.sessionStore, state.mcpManager, state.agent]);
10050
- useInput3((input, key) => {
10555
+ }, [state.session, state.sessionStore, state.mcpManager, state.agent, state.imageStore]);
10556
+ useInput4((input, key) => {
10051
10557
  if (key.ctrl && input === "c") {
10052
10558
  const now = Date.now();
10053
10559
  if (exitTimestamp && now - exitTimestamp < EXIT_TIMEOUT_MS) {
@@ -10306,9 +10812,16 @@ Remember: Use context from previous messages to understand follow-up questions.`
10306
10812
  return;
10307
10813
  }
10308
10814
  try {
10815
+ let messageContent = message;
10816
+ let userMessageContent = message;
10817
+ if (state.messageBuilder && state.messageBuilder.hasImages(message)) {
10818
+ const { message: multimodalMessage } = await state.messageBuilder.buildMessage(message);
10819
+ messageContent = multimodalMessage.content;
10820
+ userMessageContent = message;
10821
+ }
10309
10822
  const userMessage = {
10310
10823
  role: "user",
10311
- content: message,
10824
+ content: userMessageContent,
10312
10825
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
10313
10826
  };
10314
10827
  const recentMessages = state.session.messages.slice(-20);
@@ -10316,7 +10829,7 @@ Remember: Use context from previous messages to understand follow-up questions.`
10316
10829
  role: msg.role,
10317
10830
  content: msg.content
10318
10831
  }));
10319
- const result = await state.agent.run(message, {
10832
+ const result = await state.agent.run(messageContent, {
10320
10833
  previousMessages: previousMessages.length > 0 ? previousMessages : void 0
10321
10834
  });
10322
10835
  const permissionDenied = result.finalAnswer.startsWith("Permission denied for tool");
@@ -10365,6 +10878,59 @@ Remember: Use context from previous messages to understand follow-up questions.`
10365
10878
  console.error("Error processing message:", error);
10366
10879
  }
10367
10880
  };
10881
+ const handleImageDetected = async (imageData) => {
10882
+ try {
10883
+ let imageStore = state.imageStore;
10884
+ if (!imageStore) {
10885
+ if (!imageStoreInitPromise.current) {
10886
+ imageStoreInitPromise.current = (async () => {
10887
+ const { ImageStore: ImageStoreClass } = await import("./ImageStore-RQZ7OF7P.js");
10888
+ const newImageStore = new ImageStoreClass();
10889
+ setState((prev) => ({
10890
+ ...prev,
10891
+ imageStore: newImageStore,
10892
+ messageBuilder: new MessageBuilder(newImageStore, prev.imageRenderer)
10893
+ }));
10894
+ return newImageStore;
10895
+ })();
10896
+ }
10897
+ imageStore = await imageStoreInitPromise.current;
10898
+ }
10899
+ const imageRef = await imageStore.store(imageData);
10900
+ const imagePlaceholder = state.imageRenderer.createPlaceholder(imageRef.hash);
10901
+ console.log(`
10902
+ \u2713 Image detected and stored: ${imagePlaceholder}
10903
+ `);
10904
+ return imagePlaceholder;
10905
+ } catch (error) {
10906
+ console.error("Error storing image:", error);
10907
+ throw error;
10908
+ }
10909
+ };
10910
+ const recalculateSessionMetadata = (messages) => {
10911
+ let totalTokens = 0;
10912
+ let totalCost = 0;
10913
+ let toolCallCount = 0;
10914
+ for (const msg of messages) {
10915
+ if (msg.metadata) {
10916
+ if (msg.metadata.tokenUsage) {
10917
+ totalTokens += msg.metadata.tokenUsage.total || 0;
10918
+ }
10919
+ if (msg.metadata.cost) {
10920
+ totalCost += msg.metadata.cost;
10921
+ }
10922
+ if (msg.metadata.steps) {
10923
+ const observations = msg.metadata.steps.filter((s) => s.type === "observation");
10924
+ toolCallCount += observations.length;
10925
+ }
10926
+ }
10927
+ }
10928
+ return {
10929
+ totalTokens,
10930
+ totalCost,
10931
+ toolCallCount
10932
+ };
10933
+ };
10368
10934
  const handleCommand = async (command, args) => {
10369
10935
  switch (command) {
10370
10936
  case "help":
@@ -10373,6 +10939,7 @@ Available commands:
10373
10939
  /help - Show this help message
10374
10940
  /exit - Exit the CLI
10375
10941
  /clear - Start a new session
10942
+ /rewind - Rewind conversation to a previous point
10376
10943
  /login - Authenticate with your B4M account
10377
10944
  /logout - Clear authentication and sign out
10378
10945
  /whoami - Show current authenticated user
@@ -10648,6 +11215,65 @@ Project Configuration:
10648
11215
  `);
10649
11216
  break;
10650
11217
  }
11218
+ case "rewind":
11219
+ case "undo": {
11220
+ if (!state.session) {
11221
+ console.log("No active session to rewind");
11222
+ return;
11223
+ }
11224
+ if (state.session.messages.length === 0) {
11225
+ console.log("\u26A0\uFE0F Conversation is empty. Nothing to rewind.");
11226
+ return;
11227
+ }
11228
+ const userMessages = state.session.messages.filter((msg) => msg.role === "user");
11229
+ if (userMessages.length === 0) {
11230
+ console.log("\u26A0\uFE0F No user messages found. Nothing to rewind.");
11231
+ return;
11232
+ }
11233
+ if (userMessages.length === 1) {
11234
+ console.log("\u26A0\uFE0F Only one exchange in the conversation. Use /clear to start a new session instead.");
11235
+ return;
11236
+ }
11237
+ const handleRewindSelect = async (messageIndex) => {
11238
+ setState((prev) => ({ ...prev, rewindSelector: null }));
11239
+ if (messageIndex === null) {
11240
+ console.log("\u274C Rewind operation cancelled");
11241
+ return;
11242
+ }
11243
+ if (!state.session) {
11244
+ console.log("\u274C No active session");
11245
+ return;
11246
+ }
11247
+ const selectedMessage = state.session.messages[messageIndex];
11248
+ const prefillContent = selectedMessage?.content || "";
11249
+ const rewindedMessages = state.session.messages.slice(0, messageIndex);
11250
+ const newMetadata = recalculateSessionMetadata(rewindedMessages);
11251
+ const rewindedSession = {
11252
+ ...state.session,
11253
+ messages: rewindedMessages,
11254
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
11255
+ metadata: newMetadata
11256
+ };
11257
+ setState((prev) => ({
11258
+ ...prev,
11259
+ session: rewindedSession,
11260
+ prefillInput: prefillContent
11261
+ }));
11262
+ setStoreSession(rewindedSession);
11263
+ await state.sessionStore.save(rewindedSession);
11264
+ console.log("\u2705 Conversation rewound successfully");
11265
+ console.log(`\u{1F4CA} Current state: ${rewindedMessages.length} messages, ${newMetadata.totalTokens} tokens`);
11266
+ console.log(`\u{1F4DD} Your message has been placed in the input. Edit and send when ready.
11267
+ `);
11268
+ };
11269
+ setState((prev) => ({
11270
+ ...prev,
11271
+ rewindSelector: {
11272
+ resolve: handleRewindSelect
11273
+ }
11274
+ }));
11275
+ break;
11276
+ }
10651
11277
  case "usage": {
10652
11278
  const usageAuthTokens = await state.configStore.getAuthTokens();
10653
11279
  if (!usageAuthTokens || new Date(usageAuthTokens.expiresAt) <= /* @__PURE__ */ new Date()) {
@@ -10801,12 +11427,12 @@ No usage data available for the last ${USAGE_DAYS} days.`);
10801
11427
  setState((prev) => ({ ...prev, config: updatedConfig }));
10802
11428
  };
10803
11429
  if (initError) {
10804
- return /* @__PURE__ */ React13.createElement(Box13, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React13.createElement(Text12, { color: "red", bold: true }, "\u274C Initialization Error"), /* @__PURE__ */ React13.createElement(Text12, null, initError), /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, "\n", "Tip: Run /config to set up your API keys"));
11430
+ return /* @__PURE__ */ React14.createElement(Box14, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React14.createElement(Text13, { color: "red", bold: true }, "\u274C Initialization Error"), /* @__PURE__ */ React14.createElement(Text13, null, initError), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "\n", "Tip: Run /config to set up your API keys"));
10805
11431
  }
10806
11432
  if (state.trustLocationSelector) {
10807
11433
  const projectDir = state.configStore.getProjectConfigDir();
10808
11434
  const inProject = projectDir !== null;
10809
- return /* @__PURE__ */ React13.createElement(
11435
+ return /* @__PURE__ */ React14.createElement(
10810
11436
  TrustLocationSelector,
10811
11437
  {
10812
11438
  inProject,
@@ -10823,9 +11449,27 @@ No usage data available for the last ${USAGE_DAYS} days.`);
10823
11449
  }
10824
11450
  );
10825
11451
  }
11452
+ if (state.rewindSelector && state.session) {
11453
+ return /* @__PURE__ */ React14.createElement(
11454
+ RewindSelector,
11455
+ {
11456
+ messages: state.session.messages,
11457
+ onSelect: (messageIndex) => {
11458
+ if (state.rewindSelector) {
11459
+ state.rewindSelector.resolve(messageIndex);
11460
+ }
11461
+ },
11462
+ onCancel: () => {
11463
+ if (state.rewindSelector) {
11464
+ state.rewindSelector.resolve(null);
11465
+ }
11466
+ }
11467
+ }
11468
+ );
11469
+ }
10826
11470
  if (state.showLoginFlow) {
10827
11471
  const loginApiUrl = getApiUrl(state.config?.apiConfig);
10828
- return /* @__PURE__ */ React13.createElement(
11472
+ return /* @__PURE__ */ React14.createElement(
10829
11473
  LoginFlow,
10830
11474
  {
10831
11475
  apiUrl: loginApiUrl,
@@ -10848,17 +11492,22 @@ No usage data available for the last ${USAGE_DAYS} days.`);
10848
11492
  );
10849
11493
  }
10850
11494
  if (!isInitialized) {
10851
- return /* @__PURE__ */ React13.createElement(Box13, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React13.createElement(Text12, null, "\u{1F680} Initializing..."));
11495
+ return /* @__PURE__ */ React14.createElement(Box14, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React14.createElement(Text13, null, "\u{1F680} Initializing..."));
10852
11496
  }
10853
- return /* @__PURE__ */ React13.createElement(
11497
+ return /* @__PURE__ */ React14.createElement(
10854
11498
  App,
10855
11499
  {
10856
11500
  onMessage: handleMessage,
10857
11501
  onCommand: handleCommand,
11502
+ onImageDetected: handleImageDetected,
10858
11503
  commandHistory,
10859
11504
  config: state.config,
10860
11505
  availableModels: state.availableModels,
10861
11506
  onSaveConfig: handleSaveConfig,
11507
+ prefillInput: state.prefillInput,
11508
+ onPrefillConsumed: () => {
11509
+ setState((prev) => ({ ...prev, prefillInput: void 0 }));
11510
+ },
10862
11511
  onPermissionResponse: (response) => {
10863
11512
  if (state.permissionPrompt) {
10864
11513
  state.permissionPrompt.resolve({ action: response });
@@ -10884,6 +11533,6 @@ var isDevMode = import.meta.url.includes("/src/") || process.env.NODE_ENV === "d
10884
11533
  if (isDevMode) {
10885
11534
  console.log("\u{1F527} Running in development mode (using TypeScript source)\n");
10886
11535
  }
10887
- render(/* @__PURE__ */ React13.createElement(CliApp, null), {
11536
+ render(/* @__PURE__ */ React14.createElement(CliApp, null), {
10888
11537
  exitOnCtrlC: false
10889
11538
  });