@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.
- package/dist/ImageStore-LTOCD2N7.js +207 -0
- package/dist/index.js +479 -41
- package/package.json +9 -7
|
@@ -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(
|
|
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
|
|
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 (!
|
|
7308
|
+
if (!existsSync3(resolvedPath)) {
|
|
7096
7309
|
throw new Error(`File not found: ${filePath}`);
|
|
7097
7310
|
}
|
|
7098
|
-
const stats =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 (!
|
|
7812
|
+
if (!existsSync5(resolvedPath)) {
|
|
7600
7813
|
throw new Error(`File or directory not found: ${filePath}`);
|
|
7601
7814
|
}
|
|
7602
|
-
const stats =
|
|
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 =
|
|
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
|
|
8126
|
+
import { existsSync as existsSync6 } from "fs";
|
|
7914
8127
|
async function generateFileDiffPreview(args) {
|
|
7915
8128
|
try {
|
|
7916
|
-
if (!
|
|
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 (!
|
|
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
|
|
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) =>
|
|
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-
|
|
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.
|
|
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-
|
|
9284
|
-
"@bike4mind/mcp": "1.19.5-
|
|
9285
|
-
"@bike4mind/services": "2.31.1-
|
|
9286
|
-
"@bike4mind/utils": "2.0.4-
|
|
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: "
|
|
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:
|
|
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(
|
|
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-
|
|
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.
|
|
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-
|
|
106
|
-
"@bike4mind/mcp": "1.19.5-
|
|
107
|
-
"@bike4mind/services": "2.31.1-
|
|
108
|
-
"@bike4mind/utils": "2.0.4-
|
|
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": "
|
|
122
|
+
"gitHead": "b020a80effadbb22c7e0e14fcb99b9368df71f83"
|
|
121
123
|
}
|