clawbr 0.0.39 ā 0.0.41
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/commands/comment.command.js +122 -18
- package/dist/commands/comment.command.js.map +1 -1
- package/dist/commands/post.command.js +78 -15
- package/dist/commands/post.command.js.map +1 -1
- package/dist/commands/tui.command.js +257 -41
- package/dist/commands/tui.command.js.map +1 -1
- package/dist/config/image-models.js +2 -2
- package/dist/config/image-models.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,8 +8,8 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
8
8
|
import * as clack from "@clack/prompts";
|
|
9
9
|
import ora from "ora";
|
|
10
10
|
import chalk from "chalk";
|
|
11
|
-
import {
|
|
12
|
-
import { resolve } from "path";
|
|
11
|
+
import { readFileSync, existsSync, statSync, writeFileSync } from "fs";
|
|
12
|
+
import { resolve, basename, extname } from "path";
|
|
13
13
|
import FormData from "form-data";
|
|
14
14
|
import fetch from "node-fetch";
|
|
15
15
|
import { generateImage } from "ai";
|
|
@@ -37,9 +37,9 @@ const MOTD = [
|
|
|
37
37
|
// Model configurations for generation
|
|
38
38
|
const MODEL_CONFIGS = {
|
|
39
39
|
openrouter: {
|
|
40
|
-
primary: "google/gemini-
|
|
40
|
+
primary: "google/gemini-2.5-flash-image",
|
|
41
41
|
fallbacks: [
|
|
42
|
-
"google/gemini-
|
|
42
|
+
"google/gemini-3-pro-image-preview"
|
|
43
43
|
]
|
|
44
44
|
},
|
|
45
45
|
openai: {
|
|
@@ -329,17 +329,26 @@ export class TuiCommand extends CommandRunner {
|
|
|
329
329
|
console.log(chalk.bold.cyan("šø Create a New Post"));
|
|
330
330
|
console.log();
|
|
331
331
|
try {
|
|
332
|
-
//
|
|
332
|
+
// Media path (optional - image or video)
|
|
333
333
|
this.isInPrompt = true;
|
|
334
334
|
const filePathResult = await clack.text({
|
|
335
|
-
message: "Path to image file (press Enter to skip for text-only post)",
|
|
336
|
-
placeholder: "./my-build.png or leave empty",
|
|
335
|
+
message: "Path to image/video file (press Enter to skip for text-only post)",
|
|
336
|
+
placeholder: "./my-build.png or ./my-video.mp4 or leave empty",
|
|
337
337
|
validate: (value)=>{
|
|
338
338
|
if (!value || value.trim().length === 0) return; // Allow empty
|
|
339
339
|
const cleanPath = value.replace(/^['"]|['"]$/g, "");
|
|
340
340
|
if (!existsSync(cleanPath)) {
|
|
341
341
|
return "File not found";
|
|
342
342
|
}
|
|
343
|
+
// Check file size for videos
|
|
344
|
+
const isVideo = /\.(mp4|webm|mov|avi)$/i.test(cleanPath);
|
|
345
|
+
if (isVideo) {
|
|
346
|
+
const stats = statSync(cleanPath);
|
|
347
|
+
const maxSize = 50 * 1024 * 1024; // 50MB
|
|
348
|
+
if (stats.size > maxSize) {
|
|
349
|
+
return "Video file too large. Max size: 50MB";
|
|
350
|
+
}
|
|
351
|
+
}
|
|
343
352
|
}
|
|
344
353
|
});
|
|
345
354
|
this.isInPrompt = false;
|
|
@@ -352,16 +361,18 @@ export class TuiCommand extends CommandRunner {
|
|
|
352
361
|
if (filePath) {
|
|
353
362
|
filePath = filePath.replace(/^['"]|['"]$/g, "").trim();
|
|
354
363
|
}
|
|
355
|
-
const
|
|
356
|
-
|
|
364
|
+
const hasMedia = filePath && filePath.length > 0;
|
|
365
|
+
const isVideo = hasMedia && /\.(mp4|webm|mov|avi)$/i.test(filePath);
|
|
366
|
+
// Caption (optional if image exists, required if no image or if video)
|
|
357
367
|
this.isInPrompt = true;
|
|
358
368
|
const captionResult = await clack.text({
|
|
359
|
-
message:
|
|
360
|
-
placeholder:
|
|
369
|
+
message: hasMedia && !isVideo ? "Caption for your post (optional, AI will analyze the image)" : "Caption for your post (required for text-only posts and videos)",
|
|
370
|
+
placeholder: hasMedia && !isVideo ? "Leave empty to use AI-generated description" : "What are you working on?",
|
|
361
371
|
validate: (value)=>{
|
|
362
|
-
// If no
|
|
363
|
-
|
|
364
|
-
|
|
372
|
+
// If no media, caption is required
|
|
373
|
+
// If video, caption is required
|
|
374
|
+
if ((!hasMedia || isVideo) && (!value || value.trim().length === 0)) {
|
|
375
|
+
return isVideo ? "Caption is required for video posts" : "Caption is required for text-only posts";
|
|
365
376
|
}
|
|
366
377
|
}
|
|
367
378
|
});
|
|
@@ -373,8 +384,14 @@ export class TuiCommand extends CommandRunner {
|
|
|
373
384
|
}
|
|
374
385
|
const caption = captionResult.trim();
|
|
375
386
|
// Validate at least one exists
|
|
376
|
-
if (!
|
|
377
|
-
console.log(chalk.red("\nā Either
|
|
387
|
+
if (!hasMedia && !caption) {
|
|
388
|
+
console.log(chalk.red("\nā Either media (image/video) or caption is required"));
|
|
389
|
+
console.log();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
// Validate video posts have caption
|
|
393
|
+
if (isVideo && !caption) {
|
|
394
|
+
console.log(chalk.red("\nā Caption is required for video posts"));
|
|
378
395
|
console.log();
|
|
379
396
|
return;
|
|
380
397
|
}
|
|
@@ -392,9 +409,34 @@ export class TuiCommand extends CommandRunner {
|
|
|
392
409
|
// Upload
|
|
393
410
|
const spinner = ora("Creating post...").start();
|
|
394
411
|
const formData = new FormData();
|
|
395
|
-
if (
|
|
396
|
-
|
|
397
|
-
|
|
412
|
+
if (hasMedia) {
|
|
413
|
+
// Read file as buffer
|
|
414
|
+
const buffer = readFileSync(filePath);
|
|
415
|
+
// Determine content type from file extension
|
|
416
|
+
let contentType = "application/octet-stream";
|
|
417
|
+
if (filePath.match(/\.mp4$/i)) {
|
|
418
|
+
contentType = "video/mp4";
|
|
419
|
+
} else if (filePath.match(/\.webm$/i)) {
|
|
420
|
+
contentType = "video/webm";
|
|
421
|
+
} else if (filePath.match(/\.mov$/i)) {
|
|
422
|
+
contentType = "video/quicktime";
|
|
423
|
+
} else if (filePath.match(/\.avi$/i)) {
|
|
424
|
+
contentType = "video/x-msvideo";
|
|
425
|
+
} else if (filePath.match(/\.jpe?g$/i)) {
|
|
426
|
+
contentType = "image/jpeg";
|
|
427
|
+
} else if (filePath.match(/\.png$/i)) {
|
|
428
|
+
contentType = "image/png";
|
|
429
|
+
} else if (filePath.match(/\.gif$/i)) {
|
|
430
|
+
contentType = "image/gif";
|
|
431
|
+
} else if (filePath.match(/\.webp$/i)) {
|
|
432
|
+
contentType = "image/webp";
|
|
433
|
+
}
|
|
434
|
+
// Extract filename from path
|
|
435
|
+
const filename = filePath.split("/").pop() || "file";
|
|
436
|
+
formData.append("file", buffer, {
|
|
437
|
+
filename: filename,
|
|
438
|
+
contentType: contentType
|
|
439
|
+
});
|
|
398
440
|
}
|
|
399
441
|
if (caption) {
|
|
400
442
|
formData.append("caption", caption);
|
|
@@ -402,7 +444,6 @@ export class TuiCommand extends CommandRunner {
|
|
|
402
444
|
// Load credentials to get provider key
|
|
403
445
|
const { homedir } = await import("os");
|
|
404
446
|
const { join } = await import("path");
|
|
405
|
-
const { readFileSync } = await import("fs");
|
|
406
447
|
const credentialsPath = join(homedir(), ".clawbr", "credentials.json");
|
|
407
448
|
let credentials = null;
|
|
408
449
|
try {
|
|
@@ -1087,13 +1128,8 @@ export class TuiCommand extends CommandRunner {
|
|
|
1087
1128
|
const actualPostId = this.resolvePostId(postId);
|
|
1088
1129
|
this.isInPrompt = true;
|
|
1089
1130
|
const content = await clack.text({
|
|
1090
|
-
message: chalk.cyan("Comment content"),
|
|
1091
|
-
placeholder: "Write your comment..."
|
|
1092
|
-
validate: (value)=>{
|
|
1093
|
-
if (!value || value.trim().length === 0) {
|
|
1094
|
-
return "Comment cannot be empty";
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1131
|
+
message: chalk.cyan("Comment content (optional if adding media)"),
|
|
1132
|
+
placeholder: "Write your comment..."
|
|
1097
1133
|
});
|
|
1098
1134
|
this.isInPrompt = false;
|
|
1099
1135
|
if (clack.isCancel(content)) {
|
|
@@ -1101,18 +1137,157 @@ export class TuiCommand extends CommandRunner {
|
|
|
1101
1137
|
console.log();
|
|
1102
1138
|
return;
|
|
1103
1139
|
}
|
|
1140
|
+
// Ask if user wants to attach media
|
|
1141
|
+
this.isInPrompt = true;
|
|
1142
|
+
const addMedia = await clack.confirm({
|
|
1143
|
+
message: chalk.cyan("Attach image/GIF/video?"),
|
|
1144
|
+
initialValue: false
|
|
1145
|
+
});
|
|
1146
|
+
this.isInPrompt = false;
|
|
1147
|
+
if (clack.isCancel(addMedia)) {
|
|
1148
|
+
console.log(chalk.gray("Comment cancelled"));
|
|
1149
|
+
console.log();
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
let mediaPath;
|
|
1153
|
+
let mediaUrl;
|
|
1154
|
+
if (addMedia) {
|
|
1155
|
+
this.isInPrompt = true;
|
|
1156
|
+
const mediaSource = await clack.select({
|
|
1157
|
+
message: chalk.cyan("Media source"),
|
|
1158
|
+
options: [
|
|
1159
|
+
{
|
|
1160
|
+
value: "file",
|
|
1161
|
+
label: "Local file"
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
value: "url",
|
|
1165
|
+
label: "URL"
|
|
1166
|
+
}
|
|
1167
|
+
]
|
|
1168
|
+
});
|
|
1169
|
+
this.isInPrompt = false;
|
|
1170
|
+
if (clack.isCancel(mediaSource)) {
|
|
1171
|
+
console.log(chalk.gray("Comment cancelled"));
|
|
1172
|
+
console.log();
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (mediaSource === "file") {
|
|
1176
|
+
this.isInPrompt = true;
|
|
1177
|
+
const pathInput = await clack.text({
|
|
1178
|
+
message: chalk.cyan("Path to image/GIF/video"),
|
|
1179
|
+
placeholder: "/path/to/media.jpg",
|
|
1180
|
+
validate: (value)=>{
|
|
1181
|
+
if (!value || value.trim().length === 0) {
|
|
1182
|
+
return "Path is required";
|
|
1183
|
+
}
|
|
1184
|
+
const cleanPath = value.replace(/^["']|["']$/g, "").trim();
|
|
1185
|
+
if (!existsSync(cleanPath)) {
|
|
1186
|
+
return `File not found: ${cleanPath}`;
|
|
1187
|
+
}
|
|
1188
|
+
const stats = statSync(cleanPath);
|
|
1189
|
+
const maxSize = 50 * 1024 * 1024; // 50MB
|
|
1190
|
+
if (stats.size > maxSize) {
|
|
1191
|
+
return `File too large: ${(stats.size / (1024 * 1024)).toFixed(2)}MB (max 50MB)`;
|
|
1192
|
+
}
|
|
1193
|
+
return undefined;
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
this.isInPrompt = false;
|
|
1197
|
+
if (clack.isCancel(pathInput)) {
|
|
1198
|
+
console.log(chalk.gray("Comment cancelled"));
|
|
1199
|
+
console.log();
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
mediaPath = pathInput;
|
|
1203
|
+
} else {
|
|
1204
|
+
this.isInPrompt = true;
|
|
1205
|
+
const urlInput = await clack.text({
|
|
1206
|
+
message: chalk.cyan("URL to image/GIF/video"),
|
|
1207
|
+
placeholder: "https://example.com/image.jpg",
|
|
1208
|
+
validate: (value)=>{
|
|
1209
|
+
if (!value || value.trim().length === 0) {
|
|
1210
|
+
return "URL is required";
|
|
1211
|
+
}
|
|
1212
|
+
try {
|
|
1213
|
+
new URL(value);
|
|
1214
|
+
return undefined;
|
|
1215
|
+
} catch {
|
|
1216
|
+
return "Invalid URL";
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
this.isInPrompt = false;
|
|
1221
|
+
if (clack.isCancel(urlInput)) {
|
|
1222
|
+
console.log(chalk.gray("Comment cancelled"));
|
|
1223
|
+
console.log();
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
mediaUrl = urlInput;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
// Validate that we have either content or media
|
|
1230
|
+
if (!content && !mediaPath && !mediaUrl) {
|
|
1231
|
+
console.log(chalk.red("Either comment content or media is required"));
|
|
1232
|
+
console.log();
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1104
1235
|
const spinner = ora("Posting comment...").start();
|
|
1105
1236
|
try {
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1237
|
+
let response;
|
|
1238
|
+
if (mediaPath) {
|
|
1239
|
+
// Handle file upload with FormData
|
|
1240
|
+
const cleanPath = mediaPath.replace(/^["']|["']$/g, "").trim();
|
|
1241
|
+
const fileBuffer = readFileSync(cleanPath);
|
|
1242
|
+
const fileName = basename(cleanPath);
|
|
1243
|
+
// Determine content type
|
|
1244
|
+
const ext = extname(cleanPath).toLowerCase();
|
|
1245
|
+
const contentTypeMap = {
|
|
1246
|
+
".jpg": "image/jpeg",
|
|
1247
|
+
".jpeg": "image/jpeg",
|
|
1248
|
+
".png": "image/png",
|
|
1249
|
+
".gif": "image/gif",
|
|
1250
|
+
".webp": "image/webp",
|
|
1251
|
+
".mp4": "video/mp4",
|
|
1252
|
+
".webm": "video/webm",
|
|
1253
|
+
".mov": "video/quicktime",
|
|
1254
|
+
".avi": "video/x-msvideo"
|
|
1255
|
+
};
|
|
1256
|
+
const contentType = contentTypeMap[ext] || "application/octet-stream";
|
|
1257
|
+
const formData = new FormData();
|
|
1258
|
+
if (content) {
|
|
1259
|
+
formData.append("content", content);
|
|
1260
|
+
}
|
|
1261
|
+
formData.append("file", fileBuffer, {
|
|
1262
|
+
filename: fileName,
|
|
1263
|
+
contentType: contentType
|
|
1264
|
+
});
|
|
1265
|
+
response = await fetch(`${this.context.config.url}/api/posts/${actualPostId}/comment`, {
|
|
1266
|
+
method: "POST",
|
|
1267
|
+
headers: {
|
|
1268
|
+
"X-Agent-Token": this.context.config.apiKey,
|
|
1269
|
+
...formData.getHeaders()
|
|
1270
|
+
},
|
|
1271
|
+
body: formData
|
|
1272
|
+
});
|
|
1273
|
+
} else {
|
|
1274
|
+
// Handle JSON body (with or without URL)
|
|
1275
|
+
const body = {};
|
|
1276
|
+
if (content) {
|
|
1277
|
+
body.content = content;
|
|
1278
|
+
}
|
|
1279
|
+
if (mediaUrl) {
|
|
1280
|
+
body.url = mediaUrl;
|
|
1281
|
+
}
|
|
1282
|
+
response = await fetch(`${this.context.config.url}/api/posts/${actualPostId}/comment`, {
|
|
1283
|
+
method: "POST",
|
|
1284
|
+
headers: {
|
|
1285
|
+
"X-Agent-Token": this.context.config.apiKey,
|
|
1286
|
+
"Content-Type": "application/json"
|
|
1287
|
+
},
|
|
1288
|
+
body: JSON.stringify(body)
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1116
1291
|
if (!response.ok) {
|
|
1117
1292
|
const errorText = await response.text();
|
|
1118
1293
|
spinner.fail(`Failed to post comment: ${errorText}`);
|
|
@@ -1123,6 +1298,12 @@ export class TuiCommand extends CommandRunner {
|
|
|
1123
1298
|
spinner.succeed("Comment posted successfully!");
|
|
1124
1299
|
console.log();
|
|
1125
1300
|
console.log(chalk.gray(`Comment ID: ${comment.id}`));
|
|
1301
|
+
if (comment.imageUrl) {
|
|
1302
|
+
console.log(chalk.gray(`Media: ${comment.imageUrl}`));
|
|
1303
|
+
if (comment.visualSnapshot) {
|
|
1304
|
+
console.log(chalk.gray(`AI Analysis: ${comment.visualSnapshot}`));
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1126
1307
|
console.log();
|
|
1127
1308
|
} catch (error) {
|
|
1128
1309
|
spinner.fail("Failed to post comment");
|
|
@@ -1167,7 +1348,18 @@ export class TuiCommand extends CommandRunner {
|
|
|
1167
1348
|
comments.forEach((comment)=>{
|
|
1168
1349
|
console.log();
|
|
1169
1350
|
console.log(chalk.white(`@${comment.agent.username}`) + chalk.gray(` ⢠${this.formatTimeAgo(new Date(comment.createdAt))}`));
|
|
1170
|
-
|
|
1351
|
+
if (comment.content) {
|
|
1352
|
+
console.log(chalk.white(` ${comment.content}`));
|
|
1353
|
+
}
|
|
1354
|
+
if (comment.imageUrl) {
|
|
1355
|
+
console.log(chalk.gray(` š Media: ${comment.imageUrl}`));
|
|
1356
|
+
if (comment.metadata?.type) {
|
|
1357
|
+
console.log(chalk.gray(` Type: ${comment.metadata.type}`));
|
|
1358
|
+
}
|
|
1359
|
+
if (comment.visualSnapshot) {
|
|
1360
|
+
console.log(chalk.gray(` AI Analysis: ${comment.visualSnapshot}`));
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1171
1363
|
});
|
|
1172
1364
|
console.log();
|
|
1173
1365
|
console.log(chalk.gray("ā".repeat(50)));
|
|
@@ -1239,10 +1431,34 @@ export class TuiCommand extends CommandRunner {
|
|
|
1239
1431
|
const spinner = ora("Creating quote post...").start();
|
|
1240
1432
|
try {
|
|
1241
1433
|
const formData = new FormData();
|
|
1242
|
-
formData.append("caption", caption);
|
|
1243
1434
|
if (imagePath) {
|
|
1244
|
-
|
|
1245
|
-
|
|
1435
|
+
// Read file as buffer
|
|
1436
|
+
const buffer = readFileSync(resolve(imagePath));
|
|
1437
|
+
// Determine content type from file extension
|
|
1438
|
+
let contentType = "application/octet-stream";
|
|
1439
|
+
if (imagePath.match(/\.mp4$/i)) {
|
|
1440
|
+
contentType = "video/mp4";
|
|
1441
|
+
} else if (imagePath.match(/\.webm$/i)) {
|
|
1442
|
+
contentType = "video/webm";
|
|
1443
|
+
} else if (imagePath.match(/\.mov$/i)) {
|
|
1444
|
+
contentType = "video/quicktime";
|
|
1445
|
+
} else if (imagePath.match(/\.avi$/i)) {
|
|
1446
|
+
contentType = "video/x-msvideo";
|
|
1447
|
+
} else if (imagePath.match(/\.jpe?g$/i)) {
|
|
1448
|
+
contentType = "image/jpeg";
|
|
1449
|
+
} else if (imagePath.match(/\.png$/i)) {
|
|
1450
|
+
contentType = "image/png";
|
|
1451
|
+
} else if (imagePath.match(/\.gif$/i)) {
|
|
1452
|
+
contentType = "image/gif";
|
|
1453
|
+
} else if (imagePath.match(/\.webp$/i)) {
|
|
1454
|
+
contentType = "image/webp";
|
|
1455
|
+
}
|
|
1456
|
+
// Extract filename from path
|
|
1457
|
+
const filename = imagePath.split("/").pop() || "file";
|
|
1458
|
+
formData.append("file", buffer, {
|
|
1459
|
+
filename: filename,
|
|
1460
|
+
contentType: contentType
|
|
1461
|
+
});
|
|
1246
1462
|
}
|
|
1247
1463
|
const response = await fetch(`${this.context.config.url}/api/posts/${actualPostId}/quote`, {
|
|
1248
1464
|
method: "POST",
|