@bike4mind/cli 0.2.3-fix-cli-usage-code-review.17058 → 0.2.3-gh-6002-image-drap-and-drop.17054

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.
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+ import "./chunk-PDX44BCA.js";
3
+
4
+ // src/storage/ImageStore.ts
5
+ import { createHash } from "crypto";
6
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, unlinkSync } from "fs";
7
+ import { join, extname } from "path";
8
+ import { homedir } from "os";
9
+ import Database from "better-sqlite3";
10
+ import sharp from "sharp";
11
+ var ImageStore = class _ImageStore {
12
+ static {
13
+ this.BASE_DIR = join(homedir(), ".bike4mind", "cli");
14
+ }
15
+ static {
16
+ this.IMAGES_DIR = join(_ImageStore.BASE_DIR, "images");
17
+ }
18
+ static {
19
+ this.DB_PATH = join(_ImageStore.BASE_DIR, "images.db");
20
+ }
21
+ static {
22
+ this.RETENTION_DAYS = 7;
23
+ }
24
+ static {
25
+ // Target max size before base64 encoding. Base64 adds ~33% overhead.
26
+ // To stay under 1MB after base64, we need images < 750KB
27
+ this.MAX_RAW_SIZE = 750 * 1024;
28
+ }
29
+ constructor() {
30
+ this.ensureDirectories();
31
+ this.db = new Database(_ImageStore.DB_PATH);
32
+ this.initDatabase();
33
+ this.cleanupOldImages();
34
+ }
35
+ ensureDirectories() {
36
+ mkdirSync(_ImageStore.BASE_DIR, { recursive: true });
37
+ mkdirSync(_ImageStore.IMAGES_DIR, { recursive: true });
38
+ }
39
+ initDatabase() {
40
+ this.db.exec(`
41
+ CREATE TABLE IF NOT EXISTS images (
42
+ hash TEXT PRIMARY KEY,
43
+ format TEXT NOT NULL,
44
+ size INTEGER NOT NULL,
45
+ timestamp INTEGER NOT NULL,
46
+ uploaded INTEGER NOT NULL DEFAULT 0,
47
+ s3_url TEXT,
48
+ original_filename TEXT
49
+ )
50
+ `);
51
+ }
52
+ /**
53
+ * Store an image locally with content-based hashing for deduplication
54
+ */
55
+ async store(imageData, originalFilename) {
56
+ const { buffer: processedData, format: processedFormat } = await this.processImage(imageData, originalFilename);
57
+ const hash = this.generateHash(processedData);
58
+ const format = processedFormat;
59
+ const ext = format.toLowerCase();
60
+ const localPath = join(_ImageStore.IMAGES_DIR, `${hash}.${ext}`);
61
+ const existing = this.getMetadata(hash);
62
+ if (existing) {
63
+ return {
64
+ hash,
65
+ placeholder: "",
66
+ // Will be set by caller
67
+ localPath,
68
+ metadata: existing
69
+ };
70
+ }
71
+ writeFileSync(localPath, processedData);
72
+ const metadata = {
73
+ hash,
74
+ format,
75
+ size: processedData.length,
76
+ timestamp: Date.now(),
77
+ uploaded: false,
78
+ originalFilename
79
+ };
80
+ this.db.prepare(
81
+ `INSERT INTO images (hash, format, size, timestamp, uploaded, original_filename)
82
+ VALUES (?, ?, ?, ?, ?, ?)`
83
+ ).run(hash, format, metadata.size, metadata.timestamp, 0, originalFilename || null);
84
+ return {
85
+ hash,
86
+ placeholder: "",
87
+ localPath,
88
+ metadata
89
+ };
90
+ }
91
+ /**
92
+ * Get image metadata by hash
93
+ */
94
+ getMetadata(hash) {
95
+ const row = this.db.prepare("SELECT * FROM images WHERE hash = ?").get(hash);
96
+ if (!row) return null;
97
+ return {
98
+ hash: row.hash,
99
+ format: row.format,
100
+ size: row.size,
101
+ timestamp: row.timestamp,
102
+ uploaded: Boolean(row.uploaded),
103
+ s3Url: row.s3_url,
104
+ originalFilename: row.original_filename
105
+ };
106
+ }
107
+ /**
108
+ * Read image data from local cache
109
+ */
110
+ readImage(hash) {
111
+ const metadata = this.getMetadata(hash);
112
+ if (!metadata) return null;
113
+ const localPath = join(_ImageStore.IMAGES_DIR, `${hash}.${metadata.format.toLowerCase()}`);
114
+ if (!existsSync(localPath)) return null;
115
+ return readFileSync(localPath);
116
+ }
117
+ /**
118
+ * Generate content hash for deduplication
119
+ */
120
+ generateHash(data) {
121
+ return createHash("sha256").update(data).digest("hex").substring(0, 16);
122
+ }
123
+ /**
124
+ * Process image: compress if needed to stay under size limit
125
+ */
126
+ async processImage(data, originalFilename) {
127
+ const originalFormat = this.detectFormat(data, originalFilename);
128
+ if (data.length <= _ImageStore.MAX_RAW_SIZE) {
129
+ return { buffer: data, format: originalFormat };
130
+ }
131
+ let image = sharp(data);
132
+ const metadata = await image.metadata();
133
+ if (!metadata.width || !metadata.height) {
134
+ throw new Error("Unable to read image dimensions");
135
+ }
136
+ const maxDimension = 2048;
137
+ let width = metadata.width;
138
+ let height = metadata.height;
139
+ if (width > maxDimension || height > maxDimension) {
140
+ if (width > height) {
141
+ height = Math.round(height * maxDimension / width);
142
+ width = maxDimension;
143
+ } else {
144
+ width = Math.round(width * maxDimension / height);
145
+ height = maxDimension;
146
+ }
147
+ }
148
+ image = image.resize(width, height);
149
+ let quality = 85;
150
+ let buffer = await image.jpeg({ quality }).toBuffer();
151
+ while (buffer.length > _ImageStore.MAX_RAW_SIZE && quality > 30) {
152
+ quality -= 10;
153
+ buffer = await image.jpeg({ quality }).toBuffer();
154
+ }
155
+ if (buffer.length > _ImageStore.MAX_RAW_SIZE) {
156
+ throw new Error(`Unable to compress image below ${_ImageStore.MAX_RAW_SIZE / 1024}KB limit`);
157
+ }
158
+ return { buffer, format: "jpg" };
159
+ }
160
+ /**
161
+ * Detect image format from buffer or filename
162
+ */
163
+ detectFormat(data, filename) {
164
+ if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) {
165
+ return "png";
166
+ }
167
+ if (data[0] === 255 && data[1] === 216 && data[2] === 255) {
168
+ return "jpg";
169
+ }
170
+ if (data[0] === 71 && data[1] === 73 && data[2] === 70) {
171
+ return "gif";
172
+ }
173
+ if (data[0] === 82 && data[1] === 73 && data[2] === 70 && data[3] === 70) {
174
+ return "webp";
175
+ }
176
+ if (filename) {
177
+ const ext = extname(filename).substring(1).toLowerCase();
178
+ if (["png", "jpg", "jpeg", "gif", "webp"].includes(ext)) {
179
+ return ext === "jpeg" ? "jpg" : ext;
180
+ }
181
+ }
182
+ return "png";
183
+ }
184
+ /**
185
+ * Clean up images older than retention period
186
+ */
187
+ cleanupOldImages() {
188
+ const cutoffTime = Date.now() - _ImageStore.RETENTION_DAYS * 24 * 60 * 60 * 1e3;
189
+ const oldImages = this.db.prepare("SELECT hash, format FROM images WHERE timestamp < ?").all(cutoffTime);
190
+ for (const { hash, format } of oldImages) {
191
+ const localPath = join(_ImageStore.IMAGES_DIR, `${hash}.${format.toLowerCase()}`);
192
+ if (existsSync(localPath)) {
193
+ unlinkSync(localPath);
194
+ }
195
+ }
196
+ this.db.prepare("DELETE FROM images WHERE timestamp < ?").run(cutoffTime);
197
+ }
198
+ /**
199
+ * Close database connection
200
+ */
201
+ close() {
202
+ this.db.close();
203
+ }
204
+ };
205
+ export {
206
+ ImageStore
207
+ };
package/dist/index.js CHANGED
@@ -408,6 +408,202 @@ function searchCommands(query) {
408
408
  return results.map((result) => result.item);
409
409
  }
410
410
 
411
+ // src/utils/imageDetector.ts
412
+ import { readFileSync, existsSync, statSync as statSync2 } from "fs";
413
+ import { extname as extname2 } from "path";
414
+ var ImageInputDetector = class {
415
+ static {
416
+ this.MAX_IMAGE_SIZE = 10 * 1024 * 1024;
417
+ }
418
+ static {
419
+ // 10MB
420
+ this.SUPPORTED_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".webp"];
421
+ }
422
+ /**
423
+ * Check if input contains image data
424
+ */
425
+ static containsImageData(input) {
426
+ if (input.includes("\x1B]1337;File=")) {
427
+ return true;
428
+ }
429
+ if (input.includes("\x1B_G")) {
430
+ return true;
431
+ }
432
+ if (input.includes("\x1BP")) {
433
+ return true;
434
+ }
435
+ if (input.match(/data:image\/(png|jpeg|jpg|gif|webp);base64,/)) {
436
+ return true;
437
+ }
438
+ if (this.looksLikeFilePath(input)) {
439
+ return true;
440
+ }
441
+ return false;
442
+ }
443
+ /**
444
+ * Extract image data from terminal input
445
+ */
446
+ static extractImageData(input) {
447
+ const iterm2 = this.extractITerm2Image(input);
448
+ if (iterm2) return iterm2;
449
+ const kitty = this.extractKittyImage(input);
450
+ if (kitty) return kitty;
451
+ const sixel = this.extractSixelImage(input);
452
+ if (sixel) return sixel;
453
+ const base64 = this.extractBase64Image(input);
454
+ if (base64) return base64;
455
+ const fileDrop = this.extractDroppedFile(input);
456
+ if (fileDrop) return fileDrop;
457
+ return null;
458
+ }
459
+ /**
460
+ * Extract iTerm2 inline image
461
+ * Format: ESC]1337;File=[args]:base64data^G
462
+ */
463
+ static extractITerm2Image(input) {
464
+ const match = input.match(/\x1b\]1337;File=([^:]*):([^\x07]+)\x07/);
465
+ if (!match) return null;
466
+ try {
467
+ const args = match[1];
468
+ const base64Data = match[2];
469
+ const data = Buffer.from(base64Data, "base64");
470
+ let format = "png";
471
+ const nameMatch = args.match(/name=([^;]+)/);
472
+ if (nameMatch) {
473
+ const ext = extname2(nameMatch[1]).substring(1).toLowerCase();
474
+ if (ext && this.SUPPORTED_EXTENSIONS.includes(`.${ext}`)) {
475
+ format = ext === "jpeg" ? "jpg" : ext;
476
+ }
477
+ }
478
+ return {
479
+ type: "paste",
480
+ data,
481
+ format,
482
+ protocol: "iterm2"
483
+ };
484
+ } catch (error) {
485
+ return null;
486
+ }
487
+ }
488
+ /**
489
+ * Extract Kitty graphics protocol image
490
+ * Format: ESC_Ga=T,f=100;base64data ESC\
491
+ */
492
+ static extractKittyImage(input) {
493
+ const match = input.match(/\x1b_G([^;]*);([^\x1b]+)\x1b\\/);
494
+ if (!match) return null;
495
+ try {
496
+ const params = match[1];
497
+ const base64Data = match[2];
498
+ const data = Buffer.from(base64Data, "base64");
499
+ let format = "png";
500
+ const formatMatch = params.match(/f=(\d+)/);
501
+ if (formatMatch) {
502
+ const formatCode = parseInt(formatMatch[1], 10);
503
+ if (formatCode === 100) format = "jpg";
504
+ }
505
+ return {
506
+ type: "paste",
507
+ data,
508
+ format,
509
+ protocol: "kitty"
510
+ };
511
+ } catch (error) {
512
+ return null;
513
+ }
514
+ }
515
+ /**
516
+ * Extract Sixel image
517
+ * Format: ESC P q ... ESC \
518
+ */
519
+ static extractSixelImage(input) {
520
+ const match = input.match(/\x1bP([^q]*q[^\x1b]+)\x1b\\/);
521
+ if (!match) return null;
522
+ return null;
523
+ }
524
+ /**
525
+ * Extract base64 data URL
526
+ * Format: data:image/png;base64,iVBORw0KGgo...
527
+ */
528
+ static extractBase64Image(input) {
529
+ const match = input.match(/data:image\/(png|jpeg|jpg|gif|webp);base64,([A-Za-z0-9+/=]+)/);
530
+ if (!match) return null;
531
+ try {
532
+ const format = match[1] === "jpeg" ? "jpg" : match[1];
533
+ const base64Data = match[2];
534
+ const data = Buffer.from(base64Data, "base64");
535
+ return {
536
+ type: "paste",
537
+ data,
538
+ format,
539
+ protocol: "base64"
540
+ };
541
+ } catch (error) {
542
+ return null;
543
+ }
544
+ }
545
+ /**
546
+ * Extract dropped file path and read image
547
+ */
548
+ static extractDroppedFile(input) {
549
+ if (!this.looksLikeFilePath(input)) return null;
550
+ let filepath = input.trim();
551
+ filepath = filepath.replace(/^["']|["']$/g, "");
552
+ filepath = filepath.replace(/\\\s/g, " ");
553
+ if (!existsSync(filepath)) return null;
554
+ const stats = statSync2(filepath);
555
+ if (!stats.isFile()) return null;
556
+ const ext = extname2(filepath).toLowerCase();
557
+ if (!this.SUPPORTED_EXTENSIONS.includes(ext)) return null;
558
+ if (stats.size > this.MAX_IMAGE_SIZE) return null;
559
+ try {
560
+ const data = readFileSync(filepath);
561
+ const format = ext.substring(1) === "jpeg" ? "jpg" : ext.substring(1);
562
+ const filename = filepath.split("/").pop() || "image";
563
+ return {
564
+ type: "drop",
565
+ data,
566
+ format,
567
+ filename,
568
+ filepath
569
+ };
570
+ } catch (error) {
571
+ return null;
572
+ }
573
+ }
574
+ /**
575
+ * Check if string looks like a file path
576
+ */
577
+ static looksLikeFilePath(input) {
578
+ const trimmed = input.trim();
579
+ if (trimmed.startsWith("/") || trimmed.match(/^[a-zA-Z]:\\/)) {
580
+ return true;
581
+ }
582
+ if (trimmed.startsWith("~")) {
583
+ return true;
584
+ }
585
+ if (this.SUPPORTED_EXTENSIONS.some((ext) => trimmed.toLowerCase().endsWith(ext))) {
586
+ return true;
587
+ }
588
+ return false;
589
+ }
590
+ /**
591
+ * Remove image data from input string, leaving only text
592
+ */
593
+ static stripImageData(input) {
594
+ let cleaned = input;
595
+ cleaned = cleaned.replace(/\x1b\]1337;File=[^:]*:[^\x07]+\x07/g, "");
596
+ cleaned = cleaned.replace(/\x1b_G[^;]*;[^\x1b]+\x1b\\/g, "");
597
+ cleaned = cleaned.replace(/\x1bP[^q]*q[^\x1b]+\x1b\\/g, "");
598
+ cleaned = cleaned.replace(/data:image\/(png|jpeg|jpg|gif|webp);base64,[A-Za-z0-9+/=]+/g, "");
599
+ const filepath = this.extractDroppedFile(cleaned);
600
+ if (filepath) {
601
+ cleaned = cleaned.replace(filepath.filepath, "");
602
+ }
603
+ return cleaned.trim();
604
+ }
605
+ };
606
+
411
607
  // src/components/InputPrompt.tsx
412
608
  function findAtTrigger(value) {
413
609
  for (let i = value.length - 1; i >= 0; i--) {
@@ -424,7 +620,7 @@ function findAtTrigger(value) {
424
620
  }
425
621
  return null;
426
622
  }
427
- function InputPrompt({ onSubmit, disabled = false, history = [] }) {
623
+ function InputPrompt({ onSubmit, onImageDetected, disabled = false, history = [] }) {
428
624
  const [value, setValue] = useState("");
429
625
  const [selectedIndex, setSelectedIndex] = useState(0);
430
626
  const [historyIndex, setHistoryIndex] = useState(-1);
@@ -545,7 +741,16 @@ function InputPrompt({ onSubmit, disabled = false, history = [] }) {
545
741
  setHistoryIndex(-1);
546
742
  setFileAutocomplete(null);
547
743
  };
548
- const handleChange = (newValue) => {
744
+ const handleChange = async (newValue) => {
745
+ if (ImageInputDetector.containsImageData(newValue)) {
746
+ const imageEvent = ImageInputDetector.extractImageData(newValue);
747
+ if (imageEvent && onImageDetected) {
748
+ const placeholder = await onImageDetected(imageEvent.data);
749
+ setValue(`${placeholder} `);
750
+ setInputKey((prev) => prev + 1);
751
+ return;
752
+ }
753
+ }
549
754
  setValue(newValue);
550
755
  if (historyIndex !== -1) {
551
756
  setHistoryIndex(-1);
@@ -773,7 +978,7 @@ function hasFileReferences(message) {
773
978
  }
774
979
 
775
980
  // src/components/App.tsx
776
- function App({ onMessage, onCommand, onPermissionResponse, commandHistory = [] }) {
981
+ function App({ onMessage, onCommand, onPermissionResponse, onImageDetected, commandHistory = [] }) {
777
982
  const messages = useCliStore((state) => state.session?.messages || []);
778
983
  const sessionName = useCliStore((state) => state.session?.name || "New Session");
779
984
  const currentModel = useCliStore((state) => state.session?.model || "claude-sonnet-4-5-20250929");
@@ -786,7 +991,7 @@ function App({ onMessage, onCommand, onPermissionResponse, commandHistory = [] }
786
991
  async (input) => {
787
992
  const trimmed = input.trim();
788
993
  if (!trimmed) return;
789
- if (trimmed.startsWith("/")) {
994
+ if (trimmed.startsWith("/") && !trimmed.match(/^\/[A-Z]/)) {
790
995
  const [command, ...args] = trimmed.slice(1).split(" ");
791
996
  await onCommand(command, args);
792
997
  return;
@@ -820,7 +1025,15 @@ ${errorBlock}`;
820
1025
  canBeTrusted: permissionPrompt.canBeTrusted,
821
1026
  onResponse: onPermissionResponse
822
1027
  }
823
- )) : /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Static, { items: messages }, (message, index) => /* @__PURE__ */ React8.createElement(Box8, { key: index, flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React8.createElement(MessageItem, { message }))), /* @__PURE__ */ React8.createElement(AgentThinking, null), exitRequested && /* @__PURE__ */ React8.createElement(Box8, { paddingX: 1, marginBottom: 1 }, /* @__PURE__ */ React8.createElement(Text7, { color: "yellow", bold: true }, "\u26A0\uFE0F Press Ctrl+C again to exit")), /* @__PURE__ */ React8.createElement(Box8, { borderStyle: "single", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React8.createElement(InputPrompt, { onSubmit: handleSubmit, disabled: isThinking, history: commandHistory }))));
1028
+ )) : /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Static, { items: messages }, (message, index) => /* @__PURE__ */ React8.createElement(Box8, { key: index, flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React8.createElement(MessageItem, { message }))), /* @__PURE__ */ React8.createElement(AgentThinking, null), exitRequested && /* @__PURE__ */ React8.createElement(Box8, { paddingX: 1, marginBottom: 1 }, /* @__PURE__ */ React8.createElement(Text7, { color: "yellow", bold: true }, "\u26A0\uFE0F Press Ctrl+C again to exit")), /* @__PURE__ */ React8.createElement(Box8, { borderStyle: "single", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React8.createElement(
1029
+ InputPrompt,
1030
+ {
1031
+ onSubmit: handleSubmit,
1032
+ onImageDetected,
1033
+ disabled: isThinking,
1034
+ history: commandHistory
1035
+ }
1036
+ ))));
824
1037
  }
825
1038
  function MessageItem({ message }) {
826
1039
  return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React8.createElement(Box8, null, /* @__PURE__ */ React8.createElement(Text7, { bold: true, color: message.role === "user" ? "cyan" : "green" }, message.role === "user" ? "\u{1F464} You" : "\u{1F916} Assistant"), /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " \u2022 ", new Date(message.timestamp).toLocaleTimeString())), /* @__PURE__ */ React8.createElement(Box8, { paddingLeft: 2 }, message.metadata?.permissionDenied ? /* @__PURE__ */ React8.createElement(Text7, { color: "yellow" }, "\u26A0\uFE0F ", message.content) : /* @__PURE__ */ React8.createElement(Text7, null, message.content)), message.metadata?.steps && message.metadata.steps.length > 0 && /* @__PURE__ */ React8.createElement(Box8, { paddingLeft: 2, marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React8.createElement(Text7, { 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__ */ React8.createElement(Text7, { dimColor: true }, "Step types: ", message.metadata.steps.map((s) => s.type).join(", ")), message.metadata.steps.map((step, idx) => {
@@ -1632,7 +1845,7 @@ var ReActAgent = class extends EventEmitter {
1632
1845
  /**
1633
1846
  * Run the agent to completion
1634
1847
  *
1635
- * @param query - The user's question or task
1848
+ * @param query - The user's question or task (can be text or multimodal content with images)
1636
1849
  * @param options - Optional overrides for this run
1637
1850
  * @returns Agent result with final answer and all steps
1638
1851
  */
@@ -7081,7 +7294,7 @@ var planetVisibilityTool = {
7081
7294
 
7082
7295
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/fileRead/index.js
7083
7296
  import { promises as fs7 } from "fs";
7084
- import { existsSync as existsSync2, statSync as statSync3 } from "fs";
7297
+ import { existsSync as existsSync3, statSync as statSync4 } from "fs";
7085
7298
  import path6 from "path";
7086
7299
  var MAX_FILE_SIZE2 = 10 * 1024 * 1024;
7087
7300
  async function readFileContent(params) {
@@ -7092,10 +7305,10 @@ async function readFileContent(params) {
7092
7305
  if (!resolvedPath.startsWith(cwd)) {
7093
7306
  throw new Error(`Access denied: Cannot read files outside of current working directory`);
7094
7307
  }
7095
- if (!existsSync2(resolvedPath)) {
7308
+ if (!existsSync3(resolvedPath)) {
7096
7309
  throw new Error(`File not found: ${filePath}`);
7097
7310
  }
7098
- const stats = statSync3(resolvedPath);
7311
+ const stats = statSync4(resolvedPath);
7099
7312
  if (stats.isDirectory()) {
7100
7313
  throw new Error(`Path is a directory, not a file: ${filePath}`);
7101
7314
  }
@@ -7138,7 +7351,7 @@ var fileReadTool = {
7138
7351
  context.logger.info("\u{1F4C4} FileRead: Reading file", { path: params.path });
7139
7352
  try {
7140
7353
  const content = await readFileContent(params);
7141
- const stats = statSync3(path6.resolve(process.cwd(), path6.normalize(params.path)));
7354
+ const stats = statSync4(path6.resolve(process.cwd(), path6.normalize(params.path)));
7142
7355
  context.logger.info("\u2705 FileRead: Success", {
7143
7356
  path: params.path,
7144
7357
  size: stats.size,
@@ -7178,7 +7391,7 @@ var fileReadTool = {
7178
7391
 
7179
7392
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/createFile/index.js
7180
7393
  import { promises as fs8 } from "fs";
7181
- import { existsSync as existsSync3 } from "fs";
7394
+ import { existsSync as existsSync4 } from "fs";
7182
7395
  import path7 from "path";
7183
7396
  async function createFile(params) {
7184
7397
  const { path: filePath, content, createDirectories = true } = params;
@@ -7188,7 +7401,7 @@ async function createFile(params) {
7188
7401
  if (!resolvedPath.startsWith(cwd)) {
7189
7402
  throw new Error(`Access denied: Cannot create files outside of current working directory`);
7190
7403
  }
7191
- const fileExists = existsSync3(resolvedPath);
7404
+ const fileExists = existsSync4(resolvedPath);
7192
7405
  const action = fileExists ? "overwritten" : "created";
7193
7406
  if (createDirectories) {
7194
7407
  const dir = path7.dirname(resolvedPath);
@@ -7206,7 +7419,7 @@ var createFileTool = {
7206
7419
  implementation: (context) => ({
7207
7420
  toolFn: async (value) => {
7208
7421
  const params = value;
7209
- const fileExists = existsSync3(path7.resolve(process.cwd(), path7.normalize(params.path)));
7422
+ const fileExists = existsSync4(path7.resolve(process.cwd(), path7.normalize(params.path)));
7210
7423
  context.logger.info(`\u{1F4DD} CreateFile: ${fileExists ? "Overwriting" : "Creating"} file`, {
7211
7424
  path: params.path,
7212
7425
  size: params.content.length
@@ -7247,7 +7460,7 @@ var createFileTool = {
7247
7460
 
7248
7461
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/globFiles/index.js
7249
7462
  import { readdir } from "fs/promises";
7250
- import { statSync as statSync4 } from "fs";
7463
+ import { statSync as statSync5 } from "fs";
7251
7464
  import path8 from "path";
7252
7465
  function matchGlob(filePath, pattern) {
7253
7466
  let regexPattern = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/\{([^}]+)\}/g, (_, options) => `(${options.split(",").join("|")})`).replace(/\[([^\]]+)\]/g, "[$1]");
@@ -7295,7 +7508,7 @@ async function listFiles(params) {
7295
7508
  for (const match of matches) {
7296
7509
  try {
7297
7510
  const fullPath = path8.join(targetCwd, match);
7298
- const stats = statSync4(fullPath);
7511
+ const stats = statSync5(fullPath);
7299
7512
  filesInfo.push({
7300
7513
  path: match,
7301
7514
  size: stats.size,
@@ -7376,7 +7589,7 @@ var globFilesTool = {
7376
7589
 
7377
7590
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/grepSearch/index.js
7378
7591
  import { readdir as readdir2 } from "fs/promises";
7379
- import { readFileSync as readFileSync2, statSync as statSync5 } from "fs";
7592
+ import { readFileSync as readFileSync3, statSync as statSync6 } from "fs";
7380
7593
  import path9 from "path";
7381
7594
  var MAX_FILE_SIZE3 = 10 * 1024 * 1024;
7382
7595
  function matchGlob2(filePath, pattern) {
@@ -7428,7 +7641,7 @@ async function searchFiles2(params) {
7428
7641
  for (const match of filesToSearch) {
7429
7642
  const fullPath = path9.join(targetCwd, match);
7430
7643
  try {
7431
- const stats = statSync5(fullPath);
7644
+ const stats = statSync6(fullPath);
7432
7645
  if (stats.isDirectory() || stats.size > MAX_FILE_SIZE3) {
7433
7646
  continue;
7434
7647
  }
@@ -7452,7 +7665,7 @@ async function searchFiles2(params) {
7452
7665
  filesSearched++;
7453
7666
  const fullPath = path9.join(targetCwd, file);
7454
7667
  try {
7455
- const content = readFileSync2(fullPath, "utf-8");
7668
+ const content = readFileSync3(fullPath, "utf-8");
7456
7669
  const lines = content.split("\n");
7457
7670
  for (let i = 0; i < lines.length; i++) {
7458
7671
  if (matches.length >= maxResults) {
@@ -7586,7 +7799,7 @@ var grepSearchTool = {
7586
7799
 
7587
7800
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/deleteFile/index.js
7588
7801
  import { promises as fs9 } from "fs";
7589
- import { existsSync as existsSync4, statSync as statSync6 } from "fs";
7802
+ import { existsSync as existsSync5, statSync as statSync7 } from "fs";
7590
7803
  import path10 from "path";
7591
7804
  async function deleteFile(params) {
7592
7805
  const { path: filePath, recursive = false } = params;
@@ -7596,10 +7809,10 @@ async function deleteFile(params) {
7596
7809
  if (!resolvedPath.startsWith(cwd)) {
7597
7810
  throw new Error(`Access denied: Cannot delete files outside of current working directory`);
7598
7811
  }
7599
- if (!existsSync4(resolvedPath)) {
7812
+ if (!existsSync5(resolvedPath)) {
7600
7813
  throw new Error(`File or directory not found: ${filePath}`);
7601
7814
  }
7602
- const stats = statSync6(resolvedPath);
7815
+ const stats = statSync7(resolvedPath);
7603
7816
  const isDirectory = stats.isDirectory();
7604
7817
  const size = stats.size;
7605
7818
  if (isDirectory && !recursive) {
@@ -7620,7 +7833,7 @@ var deleteFileTool = {
7620
7833
  toolFn: async (value) => {
7621
7834
  const params = value;
7622
7835
  const resolvedPath = path10.resolve(process.cwd(), path10.normalize(params.path));
7623
- const isDirectory = existsSync4(resolvedPath) && statSync6(resolvedPath).isDirectory();
7836
+ const isDirectory = existsSync5(resolvedPath) && statSync7(resolvedPath).isDirectory();
7624
7837
  context.logger.info(`\u{1F5D1}\uFE0F DeleteFile: Deleting ${isDirectory ? "directory" : "file"}`, {
7625
7838
  path: params.path,
7626
7839
  recursive: params.recursive
@@ -7910,10 +8123,10 @@ import jwt2 from "jsonwebtoken";
7910
8123
  // src/utils/diffPreview.ts
7911
8124
  import * as Diff from "diff";
7912
8125
  import { readFile } from "fs/promises";
7913
- import { existsSync as existsSync5 } from "fs";
8126
+ import { existsSync as existsSync6 } from "fs";
7914
8127
  async function generateFileDiffPreview(args) {
7915
8128
  try {
7916
- if (!existsSync5(args.path)) {
8129
+ if (!existsSync6(args.path)) {
7917
8130
  const lines2 = args.content.split("\n");
7918
8131
  const preview = lines2.slice(0, 20).join("\n");
7919
8132
  const hasMore = lines2.length > 20;
@@ -7943,7 +8156,7 @@ ${preview}${hasMore ? `
7943
8156
  }
7944
8157
  async function generateFileDeletePreview(args) {
7945
8158
  try {
7946
- if (!existsSync5(args.path)) {
8159
+ if (!existsSync6(args.path)) {
7947
8160
  return `[File does not exist: ${args.path}]`;
7948
8161
  }
7949
8162
  const stats = await import("fs/promises").then((fs11) => fs11.stat(args.path));
@@ -8560,7 +8773,7 @@ var logger = Logger2.getInstance();
8560
8773
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
8561
8774
  import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
8562
8775
  import path12 from "path";
8563
- import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
8776
+ import { existsSync as existsSync7, readdirSync as readdirSync3 } from "fs";
8564
8777
  var MCPClient = class {
8565
8778
  // Note: This class handles MCP server communication with repository filtering
8566
8779
  mcp;
@@ -8596,7 +8809,7 @@ var MCPClient = class {
8596
8809
  path12.join(root, `/b4m-core/packages/mcp/dist/src/${this.serverName}/index.js`),
8597
8810
  path12.join(root, "core", "mcp", "servers", this.serverName, "dist", "index.js")
8598
8811
  ];
8599
- const serverScriptPath = candidatePaths.find((p) => existsSync6(p));
8812
+ const serverScriptPath = candidatePaths.find((p) => existsSync7(p));
8600
8813
  if (!serverScriptPath) {
8601
8814
  const getDirectories = (source) => readdirSync3(source, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
8602
8815
  console.error(`[MCP] Server script not found. Tried paths:`, candidatePaths);
@@ -8792,6 +9005,190 @@ var McpManager = class {
8792
9005
  }
8793
9006
  };
8794
9007
 
9008
+ // src/utils/imageRenderer.ts
9009
+ var ImageRenderer = class {
9010
+ constructor() {
9011
+ this.imageCounter = 0;
9012
+ this.placeholderMap = /* @__PURE__ */ new Map();
9013
+ }
9014
+ /**
9015
+ * Generate placeholder for a new image
9016
+ */
9017
+ createPlaceholder(hash) {
9018
+ const existing = this.placeholderMap.get(hash);
9019
+ if (existing) {
9020
+ return existing.placeholder;
9021
+ }
9022
+ this.imageCounter++;
9023
+ const placeholder = `[Image ${this.imageCounter}]`;
9024
+ this.placeholderMap.set(hash, {
9025
+ hash,
9026
+ number: this.imageCounter,
9027
+ placeholder
9028
+ });
9029
+ return placeholder;
9030
+ }
9031
+ /**
9032
+ * Get all image hashes from a message containing placeholders
9033
+ */
9034
+ extractImageHashes(message) {
9035
+ const hashes = [];
9036
+ const placeholderRegex = /\[Image \d+\]/g;
9037
+ const matches = message.match(placeholderRegex);
9038
+ if (!matches) return hashes;
9039
+ for (const [hash, data] of this.placeholderMap.entries()) {
9040
+ if (matches.includes(data.placeholder)) {
9041
+ hashes.push(hash);
9042
+ }
9043
+ }
9044
+ return hashes;
9045
+ }
9046
+ /**
9047
+ * Get hash by placeholder text
9048
+ */
9049
+ getHashFromPlaceholder(placeholder) {
9050
+ for (const [hash, data] of this.placeholderMap.entries()) {
9051
+ if (data.placeholder === placeholder) {
9052
+ return hash;
9053
+ }
9054
+ }
9055
+ return null;
9056
+ }
9057
+ };
9058
+
9059
+ // src/utils/messageBuilder.ts
9060
+ var MessageBuilder = class {
9061
+ constructor(imageStore, imageRenderer) {
9062
+ this.imageStore = imageStore;
9063
+ this.imageRenderer = imageRenderer;
9064
+ }
9065
+ /**
9066
+ * Build a multimodal message from user input
9067
+ * Extracts image placeholders and creates proper message structure with base64-encoded images
9068
+ */
9069
+ async buildMessage(userInput, role = "user") {
9070
+ const imageHashes = this.imageRenderer.extractImageHashes(userInput);
9071
+ if (imageHashes.length === 0) {
9072
+ return {
9073
+ message: {
9074
+ role,
9075
+ content: userInput
9076
+ },
9077
+ uploadedImages: []
9078
+ };
9079
+ }
9080
+ const content = [];
9081
+ const parts = this.splitMessageByPlaceholders(userInput);
9082
+ for (const part of parts) {
9083
+ if (part.type === "text" && part.content.trim()) {
9084
+ content.push({
9085
+ type: "text",
9086
+ text: part.content
9087
+ });
9088
+ } else if (part.type === "image") {
9089
+ const hash = this.imageRenderer.getHashFromPlaceholder(part.content);
9090
+ if (hash) {
9091
+ const imageData = this.imageStore.readImage(hash);
9092
+ const metadata = this.imageStore.getMetadata(hash);
9093
+ if (imageData && metadata) {
9094
+ const base64Data = imageData.toString("base64");
9095
+ const mediaType = this.getMediaType(metadata.format);
9096
+ content.push({
9097
+ type: "image",
9098
+ source: {
9099
+ type: "base64",
9100
+ media_type: mediaType,
9101
+ data: base64Data
9102
+ }
9103
+ });
9104
+ }
9105
+ }
9106
+ }
9107
+ }
9108
+ return {
9109
+ message: {
9110
+ role,
9111
+ content
9112
+ },
9113
+ uploadedImages: []
9114
+ };
9115
+ }
9116
+ /**
9117
+ * Get MIME type for image format
9118
+ */
9119
+ getMediaType(format) {
9120
+ switch (format.toLowerCase()) {
9121
+ case "png":
9122
+ return "image/png";
9123
+ case "jpg":
9124
+ case "jpeg":
9125
+ return "image/jpeg";
9126
+ case "gif":
9127
+ return "image/gif";
9128
+ case "webp":
9129
+ return "image/webp";
9130
+ default:
9131
+ return "image/png";
9132
+ }
9133
+ }
9134
+ /**
9135
+ * Split message text by image placeholders
9136
+ * Returns array of text and image parts in order
9137
+ */
9138
+ splitMessageByPlaceholders(message) {
9139
+ const parts = [];
9140
+ const placeholderRegex = /(\[Image \d+\])/g;
9141
+ let lastIndex = 0;
9142
+ let match;
9143
+ while ((match = placeholderRegex.exec(message)) !== null) {
9144
+ if (match.index > lastIndex) {
9145
+ const text = message.substring(lastIndex, match.index);
9146
+ if (text) {
9147
+ parts.push({ type: "text", content: text });
9148
+ }
9149
+ }
9150
+ parts.push({ type: "image", content: match[0] });
9151
+ lastIndex = match.index + match[0].length;
9152
+ }
9153
+ if (lastIndex < message.length) {
9154
+ const text = message.substring(lastIndex);
9155
+ if (text) {
9156
+ parts.push({ type: "text", content: text });
9157
+ }
9158
+ }
9159
+ if (parts.length === 0) {
9160
+ parts.push({ type: "text", content: message });
9161
+ }
9162
+ return parts;
9163
+ }
9164
+ /**
9165
+ * Check if a message contains images
9166
+ */
9167
+ hasImages(message) {
9168
+ return this.imageRenderer.extractImageHashes(message).length > 0;
9169
+ }
9170
+ /**
9171
+ * Convert multimodal message back to simple string (for display/logging)
9172
+ */
9173
+ messageToString(message) {
9174
+ if (typeof message.content === "string") {
9175
+ return message.content;
9176
+ }
9177
+ return message.content.map((item) => {
9178
+ if (item.type === "text") {
9179
+ return item.text;
9180
+ } else if (item.type === "image") {
9181
+ if (item.source.type === "url") {
9182
+ return `[Image: ${item.source.url}]`;
9183
+ } else {
9184
+ return `[Image: base64 ${item.source.media_type}]`;
9185
+ }
9186
+ }
9187
+ return "";
9188
+ }).join("");
9189
+ }
9190
+ };
9191
+
8795
9192
  // src/llm/ServerLlmBackend.ts
8796
9193
  import { createParser } from "eventsource-parser";
8797
9194
  import { isAxiosError } from "axios";
@@ -9178,7 +9575,7 @@ import { isAxiosError as isAxiosError2 } from "axios";
9178
9575
  // package.json
9179
9576
  var package_default = {
9180
9577
  name: "@bike4mind/cli",
9181
- version: "0.2.3-fix-cli-usage-code-review.17058+2e5763de7",
9578
+ version: "0.2.3-gh-6002-image-drap-and-drop.17054+b020a80ef",
9182
9579
  type: "module",
9183
9580
  description: "Interactive CLI tool for Bike4Mind with ReAct agents",
9184
9581
  license: "UNLICENSED",
@@ -9239,6 +9636,7 @@ var package_default = {
9239
9636
  "async-mutex": "^0.5.0",
9240
9637
  axios: "^1.13.2",
9241
9638
  bcryptjs: "^2.4.3",
9639
+ "better-sqlite3": "^12.5.0",
9242
9640
  cheerio: "1.0.0-rc.12",
9243
9641
  "csv-parse": "^5.5.6",
9244
9642
  dayjs: "^1.11.13",
@@ -9264,7 +9662,7 @@ var package_default = {
9264
9662
  "p-limit": "^6.2.0",
9265
9663
  qrcode: "^1.5.4",
9266
9664
  react: "^19.2.3",
9267
- sharp: "^0.32.6",
9665
+ sharp: "^0.34.5",
9268
9666
  speakeasy: "^2.0.0",
9269
9667
  tiktoken: "^1.0.16",
9270
9668
  turndown: "^7.2.0",
@@ -9280,10 +9678,11 @@ var package_default = {
9280
9678
  },
9281
9679
  devDependencies: {
9282
9680
  "@bike4mind/agents": "0.1.0",
9283
- "@bike4mind/common": "2.37.1-fix-cli-usage-code-review.17058+2e5763de7",
9284
- "@bike4mind/mcp": "1.19.5-fix-cli-usage-code-review.17058+2e5763de7",
9285
- "@bike4mind/services": "2.31.1-fix-cli-usage-code-review.17058+2e5763de7",
9286
- "@bike4mind/utils": "2.0.4-fix-cli-usage-code-review.17058+2e5763de7",
9681
+ "@bike4mind/common": "2.37.1-gh-6002-image-drap-and-drop.17054+b020a80ef",
9682
+ "@bike4mind/mcp": "1.19.5-gh-6002-image-drap-and-drop.17054+b020a80ef",
9683
+ "@bike4mind/services": "2.31.1-gh-6002-image-drap-and-drop.17054+b020a80ef",
9684
+ "@bike4mind/utils": "2.0.4-gh-6002-image-drap-and-drop.17054+b020a80ef",
9685
+ "@types/better-sqlite3": "^7.6.12",
9287
9686
  "@types/diff": "^5.0.9",
9288
9687
  "@types/jsonwebtoken": "^9.0.4",
9289
9688
  "@types/node": "^22.9.0",
@@ -9295,7 +9694,7 @@ var package_default = {
9295
9694
  typescript: "^5.9.3",
9296
9695
  vitest: "^3.2.4"
9297
9696
  },
9298
- gitHead: "2e5763de75948174c0c15633b8107371b0365e24"
9697
+ gitHead: "b020a80effadbb22c7e0e14fcb99b9368df71f83"
9299
9698
  };
9300
9699
 
9301
9700
  // src/config/constants.ts
@@ -9316,11 +9715,16 @@ var EXIT_TIMEOUT_MS = 2e3;
9316
9715
  var usageCache = null;
9317
9716
  function CliApp() {
9318
9717
  const { exit } = useApp();
9718
+ const imageRenderer = new ImageRenderer();
9319
9719
  const [state, setState] = useState3({
9320
9720
  session: null,
9321
9721
  sessionStore: new SessionStore(),
9322
9722
  configStore: new ConfigStore(),
9323
9723
  commandHistoryStore: new CommandHistoryStore(),
9724
+ imageStore: null,
9725
+ // Lazy-loaded on first use
9726
+ imageRenderer,
9727
+ messageBuilder: null,
9324
9728
  agent: null,
9325
9729
  mcpManager: null,
9326
9730
  permissionManager: null,
@@ -9351,6 +9755,13 @@ function CliApp() {
9351
9755
  if (state.agent) {
9352
9756
  state.agent.removeAllListeners();
9353
9757
  }
9758
+ if (state.imageStore) {
9759
+ try {
9760
+ state.imageStore.close();
9761
+ } catch (err) {
9762
+ logger.debug(`[CLEANUP] Image store close error: ${err}`);
9763
+ }
9764
+ }
9354
9765
  await Promise.race([
9355
9766
  Promise.all(cleanupTasks),
9356
9767
  new Promise((resolve3) => setTimeout(resolve3, 500))
@@ -9359,7 +9770,7 @@ function CliApp() {
9359
9770
  setTimeout(() => {
9360
9771
  process.exit(0);
9361
9772
  }, 100);
9362
- }, [state.session, state.sessionStore, state.mcpManager, state.agent]);
9773
+ }, [state.session, state.sessionStore, state.mcpManager, state.agent, state.imageStore]);
9363
9774
  useInput2((input, key) => {
9364
9775
  if (key.ctrl && input === "c") {
9365
9776
  const now = Date.now();
@@ -9609,9 +10020,16 @@ Remember: Use context from previous messages to understand follow-up questions.`
9609
10020
  return;
9610
10021
  }
9611
10022
  try {
10023
+ let messageContent = message;
10024
+ let userMessageContent = message;
10025
+ if (state.messageBuilder && state.messageBuilder.hasImages(message)) {
10026
+ const { message: multimodalMessage } = await state.messageBuilder.buildMessage(message);
10027
+ messageContent = multimodalMessage.content;
10028
+ userMessageContent = message;
10029
+ }
9612
10030
  const userMessage = {
9613
10031
  role: "user",
9614
- content: message,
10032
+ content: userMessageContent,
9615
10033
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
9616
10034
  };
9617
10035
  const recentMessages = state.session.messages.slice(-20);
@@ -9619,7 +10037,7 @@ Remember: Use context from previous messages to understand follow-up questions.`
9619
10037
  role: msg.role,
9620
10038
  content: msg.content
9621
10039
  }));
9622
- const result = await state.agent.run(message, {
10040
+ const result = await state.agent.run(messageContent, {
9623
10041
  previousMessages: previousMessages.length > 0 ? previousMessages : void 0
9624
10042
  });
9625
10043
  const permissionDenied = result.finalAnswer.startsWith("Permission denied for tool");
@@ -9668,6 +10086,29 @@ Remember: Use context from previous messages to understand follow-up questions.`
9668
10086
  console.error("Error processing message:", error);
9669
10087
  }
9670
10088
  };
10089
+ const handleImageDetected = async (imageData) => {
10090
+ try {
10091
+ let imageStore = state.imageStore;
10092
+ if (!imageStore) {
10093
+ const { ImageStore: ImageStoreClass } = await import("./ImageStore-LTOCD2N7.js");
10094
+ imageStore = new ImageStoreClass();
10095
+ setState((prev) => ({
10096
+ ...prev,
10097
+ imageStore,
10098
+ messageBuilder: new MessageBuilder(imageStore, prev.imageRenderer)
10099
+ }));
10100
+ }
10101
+ const imageRef = await imageStore.store(imageData);
10102
+ const imagePlaceholder = state.imageRenderer.createPlaceholder(imageRef.hash);
10103
+ console.log(`
10104
+ \u2713 Image detected and stored: ${imagePlaceholder}
10105
+ `);
10106
+ return imagePlaceholder;
10107
+ } catch (error) {
10108
+ console.error("Error storing image:", error);
10109
+ throw error;
10110
+ }
10111
+ };
9671
10112
  const handleCommand = async (command, args) => {
9672
10113
  switch (command) {
9673
10114
  case "help":
@@ -9833,7 +10274,6 @@ Tool Permissions:
9833
10274
  setState((prev) => ({ ...prev, showLoginFlow: true }));
9834
10275
  break;
9835
10276
  case "logout":
9836
- usageCache = null;
9837
10277
  await state.configStore.clearAuthTokens();
9838
10278
  console.log("\u2705 Successfully logged out");
9839
10279
  break;
@@ -9873,9 +10313,6 @@ Tool Permissions:
9873
10313
  apiClient.get("/api/identify"),
9874
10314
  apiClient.get(`/api/credits/transactions?days=${USAGE_DAYS}`)
9875
10315
  ]);
9876
- if (!identifyResponse?.user?.currentCredits && identifyResponse?.user?.currentCredits !== 0) {
9877
- throw new Error("Invalid response format from /api/identify");
9878
- }
9879
10316
  currentCredits = identifyResponse.user.currentCredits;
9880
10317
  transactions = transactionsResponse || [];
9881
10318
  usageCache = {
@@ -9990,6 +10427,7 @@ No usage data available for the last ${USAGE_DAYS} days.`);
9990
10427
  {
9991
10428
  onMessage: handleMessage,
9992
10429
  onCommand: handleCommand,
10430
+ onImageDetected: handleImageDetected,
9993
10431
  commandHistory,
9994
10432
  onPermissionResponse: (response) => {
9995
10433
  if (state.permissionPrompt) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bike4mind/cli",
3
- "version": "0.2.3-fix-cli-usage-code-review.17058+2e5763de7",
3
+ "version": "0.2.3-gh-6002-image-drap-and-drop.17054+b020a80ef",
4
4
  "type": "module",
5
5
  "description": "Interactive CLI tool for Bike4Mind with ReAct agents",
6
6
  "license": "UNLICENSED",
@@ -61,6 +61,7 @@
61
61
  "async-mutex": "^0.5.0",
62
62
  "axios": "^1.13.2",
63
63
  "bcryptjs": "^2.4.3",
64
+ "better-sqlite3": "^12.5.0",
64
65
  "cheerio": "1.0.0-rc.12",
65
66
  "csv-parse": "^5.5.6",
66
67
  "dayjs": "^1.11.13",
@@ -86,7 +87,7 @@
86
87
  "p-limit": "^6.2.0",
87
88
  "qrcode": "^1.5.4",
88
89
  "react": "^19.2.3",
89
- "sharp": "^0.32.6",
90
+ "sharp": "^0.34.5",
90
91
  "speakeasy": "^2.0.0",
91
92
  "tiktoken": "^1.0.16",
92
93
  "turndown": "^7.2.0",
@@ -102,10 +103,11 @@
102
103
  },
103
104
  "devDependencies": {
104
105
  "@bike4mind/agents": "0.1.0",
105
- "@bike4mind/common": "2.37.1-fix-cli-usage-code-review.17058+2e5763de7",
106
- "@bike4mind/mcp": "1.19.5-fix-cli-usage-code-review.17058+2e5763de7",
107
- "@bike4mind/services": "2.31.1-fix-cli-usage-code-review.17058+2e5763de7",
108
- "@bike4mind/utils": "2.0.4-fix-cli-usage-code-review.17058+2e5763de7",
106
+ "@bike4mind/common": "2.37.1-gh-6002-image-drap-and-drop.17054+b020a80ef",
107
+ "@bike4mind/mcp": "1.19.5-gh-6002-image-drap-and-drop.17054+b020a80ef",
108
+ "@bike4mind/services": "2.31.1-gh-6002-image-drap-and-drop.17054+b020a80ef",
109
+ "@bike4mind/utils": "2.0.4-gh-6002-image-drap-and-drop.17054+b020a80ef",
110
+ "@types/better-sqlite3": "^7.6.12",
109
111
  "@types/diff": "^5.0.9",
110
112
  "@types/jsonwebtoken": "^9.0.4",
111
113
  "@types/node": "^22.9.0",
@@ -117,5 +119,5 @@
117
119
  "typescript": "^5.9.3",
118
120
  "vitest": "^3.2.4"
119
121
  },
120
- "gitHead": "2e5763de75948174c0c15633b8107371b0365e24"
122
+ "gitHead": "b020a80effadbb22c7e0e14fcb99b9368df71f83"
121
123
  }