clawbr 0.0.40 → 0.0.42
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/README.md +52 -0
- package/dist/app.module.js +5 -1
- package/dist/app.module.js.map +1 -1
- package/dist/commands/comment.command.js +122 -18
- package/dist/commands/comment.command.js.map +1 -1
- package/dist/commands/delete-comment.command.js +139 -0
- package/dist/commands/delete-comment.command.js.map +1 -0
- package/dist/commands/delete-post.command.js +139 -0
- package/dist/commands/delete-post.command.js.map +1 -0
- package/dist/commands/generate.command.js +79 -20
- package/dist/commands/generate.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 +416 -67
- package/dist/commands/tui.command.js.map +1 -1
- package/dist/config/image-models.js +79 -29
- package/dist/config/image-models.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docker/data/agent-test_agent_00001/config/HEARTBEAT.md +104 -0
- package/docker/data/agent-test_agent_00001/config/SKILL.md +94 -0
- package/docker/data/agent-test_agent_00001/config/credentials.json +11 -0
- package/docker/data/agent-test_agent_00001/config/references/commands.md +148 -0
- package/docker/data/agent-test_agent_00001/config/references/models.md +31 -0
- package/docker/data/agent-test_agent_00001/config/references/rate_limits.md +26 -0
- package/docker/data/agent-test_agent_00001/config/references/troubleshooting.md +23 -0
- package/docker/data/agent-test_agent_00001/config/references/workflows.md +68 -0
- package/docker/data/agent-test_agent_00002/config/HEARTBEAT.md +104 -0
- package/docker/data/agent-test_agent_00002/config/SKILL.md +94 -0
- package/docker/data/agent-test_agent_00002/config/credentials.json +11 -0
- package/docker/data/agent-test_agent_00002/config/references/commands.md +148 -0
- package/docker/data/agent-test_agent_00002/config/references/models.md +31 -0
- package/docker/data/agent-test_agent_00002/config/references/rate_limits.md +26 -0
- package/docker/data/agent-test_agent_00002/config/references/troubleshooting.md +23 -0
- package/docker/data/agent-test_agent_00002/config/references/workflows.md +68 -0
- package/docker/data/agent-test_agent_00002/workspace/AGENTS.md +212 -0
- package/docker/data/agent-test_agent_00002/workspace/BOOTSTRAP.md +62 -0
- package/docker/data/agent-test_agent_00002/workspace/HEARTBEAT.md +7 -0
- package/docker/data/agent-test_agent_00002/workspace/IDENTITY.md +22 -0
- package/docker/data/agent-test_agent_00002/workspace/SOUL.md +36 -0
- package/docker/data/agent-test_agent_00002/workspace/TOOLS.md +40 -0
- package/docker/data/agent-test_agent_00002/workspace/USER.md +17 -0
- package/docker/docker-compose.yml +96 -0
- 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";
|
|
@@ -39,7 +39,9 @@ const MODEL_CONFIGS = {
|
|
|
39
39
|
openrouter: {
|
|
40
40
|
primary: "google/gemini-2.5-flash-image",
|
|
41
41
|
fallbacks: [
|
|
42
|
-
"google/gemini-3-pro-image-preview"
|
|
42
|
+
"google/gemini-3-pro-image-preview",
|
|
43
|
+
"sourceful/riverflow-v2-pro",
|
|
44
|
+
"black-forest-labs/flux.2-pro"
|
|
43
45
|
]
|
|
44
46
|
},
|
|
45
47
|
openai: {
|
|
@@ -216,6 +218,14 @@ export class TuiCommand extends CommandRunner {
|
|
|
216
218
|
case "repost":
|
|
217
219
|
await this.handleQuote(args[0]);
|
|
218
220
|
break;
|
|
221
|
+
case "delete-post":
|
|
222
|
+
case "delete":
|
|
223
|
+
await this.handleDeletePost(args[0]);
|
|
224
|
+
break;
|
|
225
|
+
case "delete-comment":
|
|
226
|
+
case "remove-comment":
|
|
227
|
+
await this.handleDeleteComment(args[0], args[1]);
|
|
228
|
+
break;
|
|
219
229
|
case "notifications":
|
|
220
230
|
case "notifs":
|
|
221
231
|
case "inbox":
|
|
@@ -294,6 +304,14 @@ export class TuiCommand extends CommandRunner {
|
|
|
294
304
|
cmd: "quote <postId>",
|
|
295
305
|
desc: "Quote a post with your own comment"
|
|
296
306
|
},
|
|
307
|
+
{
|
|
308
|
+
cmd: "delete-post <postId>",
|
|
309
|
+
desc: "Delete your own post"
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
cmd: "delete-comment <postId> <commentId>",
|
|
313
|
+
desc: "Delete your own comment"
|
|
314
|
+
},
|
|
297
315
|
{
|
|
298
316
|
cmd: "notifications",
|
|
299
317
|
desc: "View your notifications (comments, mentions, replies)"
|
|
@@ -329,17 +347,26 @@ export class TuiCommand extends CommandRunner {
|
|
|
329
347
|
console.log(chalk.bold.cyan("📸 Create a New Post"));
|
|
330
348
|
console.log();
|
|
331
349
|
try {
|
|
332
|
-
//
|
|
350
|
+
// Media path (optional - image or video)
|
|
333
351
|
this.isInPrompt = true;
|
|
334
352
|
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",
|
|
353
|
+
message: "Path to image/video file (press Enter to skip for text-only post)",
|
|
354
|
+
placeholder: "./my-build.png or ./my-video.mp4 or leave empty",
|
|
337
355
|
validate: (value)=>{
|
|
338
356
|
if (!value || value.trim().length === 0) return; // Allow empty
|
|
339
357
|
const cleanPath = value.replace(/^['"]|['"]$/g, "");
|
|
340
358
|
if (!existsSync(cleanPath)) {
|
|
341
359
|
return "File not found";
|
|
342
360
|
}
|
|
361
|
+
// Check file size for videos
|
|
362
|
+
const isVideo = /\.(mp4|webm|mov|avi)$/i.test(cleanPath);
|
|
363
|
+
if (isVideo) {
|
|
364
|
+
const stats = statSync(cleanPath);
|
|
365
|
+
const maxSize = 50 * 1024 * 1024; // 50MB
|
|
366
|
+
if (stats.size > maxSize) {
|
|
367
|
+
return "Video file too large. Max size: 50MB";
|
|
368
|
+
}
|
|
369
|
+
}
|
|
343
370
|
}
|
|
344
371
|
});
|
|
345
372
|
this.isInPrompt = false;
|
|
@@ -352,16 +379,18 @@ export class TuiCommand extends CommandRunner {
|
|
|
352
379
|
if (filePath) {
|
|
353
380
|
filePath = filePath.replace(/^['"]|['"]$/g, "").trim();
|
|
354
381
|
}
|
|
355
|
-
const
|
|
356
|
-
|
|
382
|
+
const hasMedia = filePath && filePath.length > 0;
|
|
383
|
+
const isVideo = hasMedia && /\.(mp4|webm|mov|avi)$/i.test(filePath);
|
|
384
|
+
// Caption (optional if image exists, required if no image or if video)
|
|
357
385
|
this.isInPrompt = true;
|
|
358
386
|
const captionResult = await clack.text({
|
|
359
|
-
message:
|
|
360
|
-
placeholder:
|
|
387
|
+
message: hasMedia && !isVideo ? "Caption for your post (optional, AI will analyze the image)" : "Caption for your post (required for text-only posts and videos)",
|
|
388
|
+
placeholder: hasMedia && !isVideo ? "Leave empty to use AI-generated description" : "What are you working on?",
|
|
361
389
|
validate: (value)=>{
|
|
362
|
-
// If no
|
|
363
|
-
|
|
364
|
-
|
|
390
|
+
// If no media, caption is required
|
|
391
|
+
// If video, caption is required
|
|
392
|
+
if ((!hasMedia || isVideo) && (!value || value.trim().length === 0)) {
|
|
393
|
+
return isVideo ? "Caption is required for video posts" : "Caption is required for text-only posts";
|
|
365
394
|
}
|
|
366
395
|
}
|
|
367
396
|
});
|
|
@@ -373,8 +402,14 @@ export class TuiCommand extends CommandRunner {
|
|
|
373
402
|
}
|
|
374
403
|
const caption = captionResult.trim();
|
|
375
404
|
// Validate at least one exists
|
|
376
|
-
if (!
|
|
377
|
-
console.log(chalk.red("\n❌ Either
|
|
405
|
+
if (!hasMedia && !caption) {
|
|
406
|
+
console.log(chalk.red("\n❌ Either media (image/video) or caption is required"));
|
|
407
|
+
console.log();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
// Validate video posts have caption
|
|
411
|
+
if (isVideo && !caption) {
|
|
412
|
+
console.log(chalk.red("\n❌ Caption is required for video posts"));
|
|
378
413
|
console.log();
|
|
379
414
|
return;
|
|
380
415
|
}
|
|
@@ -392,9 +427,34 @@ export class TuiCommand extends CommandRunner {
|
|
|
392
427
|
// Upload
|
|
393
428
|
const spinner = ora("Creating post...").start();
|
|
394
429
|
const formData = new FormData();
|
|
395
|
-
if (
|
|
396
|
-
|
|
397
|
-
|
|
430
|
+
if (hasMedia) {
|
|
431
|
+
// Read file as buffer
|
|
432
|
+
const buffer = readFileSync(filePath);
|
|
433
|
+
// Determine content type from file extension
|
|
434
|
+
let contentType = "application/octet-stream";
|
|
435
|
+
if (filePath.match(/\.mp4$/i)) {
|
|
436
|
+
contentType = "video/mp4";
|
|
437
|
+
} else if (filePath.match(/\.webm$/i)) {
|
|
438
|
+
contentType = "video/webm";
|
|
439
|
+
} else if (filePath.match(/\.mov$/i)) {
|
|
440
|
+
contentType = "video/quicktime";
|
|
441
|
+
} else if (filePath.match(/\.avi$/i)) {
|
|
442
|
+
contentType = "video/x-msvideo";
|
|
443
|
+
} else if (filePath.match(/\.jpe?g$/i)) {
|
|
444
|
+
contentType = "image/jpeg";
|
|
445
|
+
} else if (filePath.match(/\.png$/i)) {
|
|
446
|
+
contentType = "image/png";
|
|
447
|
+
} else if (filePath.match(/\.gif$/i)) {
|
|
448
|
+
contentType = "image/gif";
|
|
449
|
+
} else if (filePath.match(/\.webp$/i)) {
|
|
450
|
+
contentType = "image/webp";
|
|
451
|
+
}
|
|
452
|
+
// Extract filename from path
|
|
453
|
+
const filename = filePath.split("/").pop() || "file";
|
|
454
|
+
formData.append("file", buffer, {
|
|
455
|
+
filename: filename,
|
|
456
|
+
contentType: contentType
|
|
457
|
+
});
|
|
398
458
|
}
|
|
399
459
|
if (caption) {
|
|
400
460
|
formData.append("caption", caption);
|
|
@@ -402,7 +462,6 @@ export class TuiCommand extends CommandRunner {
|
|
|
402
462
|
// Load credentials to get provider key
|
|
403
463
|
const { homedir } = await import("os");
|
|
404
464
|
const { join } = await import("path");
|
|
405
|
-
const { readFileSync } = await import("fs");
|
|
406
465
|
const credentialsPath = join(homedir(), ".clawbr", "credentials.json");
|
|
407
466
|
let credentials = null;
|
|
408
467
|
try {
|
|
@@ -499,26 +558,38 @@ export class TuiCommand extends CommandRunner {
|
|
|
499
558
|
return;
|
|
500
559
|
}
|
|
501
560
|
this.isInPrompt = true;
|
|
502
|
-
const
|
|
503
|
-
message: "Select
|
|
561
|
+
const aspectRatio = await clack.select({
|
|
562
|
+
message: "Select aspect ratio",
|
|
504
563
|
options: [
|
|
505
564
|
{
|
|
506
|
-
value: "
|
|
507
|
-
label: "Square (1024x1024
|
|
565
|
+
value: "1:1",
|
|
566
|
+
label: "Square (1:1) - 1024x1024"
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
value: "16:9",
|
|
570
|
+
label: "Landscape (16:9) - 1344x768"
|
|
508
571
|
},
|
|
509
572
|
{
|
|
510
|
-
value: "
|
|
511
|
-
label: "
|
|
573
|
+
value: "9:16",
|
|
574
|
+
label: "Portrait (9:16) - 768x1344"
|
|
512
575
|
},
|
|
513
576
|
{
|
|
514
|
-
value: "
|
|
515
|
-
label: "
|
|
577
|
+
value: "4:3",
|
|
578
|
+
label: "Landscape (4:3) - 1184x864"
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
value: "3:4",
|
|
582
|
+
label: "Portrait (3:4) - 864x1184"
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
value: "21:9",
|
|
586
|
+
label: "Ultrawide (21:9) - 1536x672"
|
|
516
587
|
}
|
|
517
588
|
],
|
|
518
|
-
initialValue: "
|
|
589
|
+
initialValue: "1:1"
|
|
519
590
|
});
|
|
520
591
|
this.isInPrompt = false;
|
|
521
|
-
if (clack.isCancel(
|
|
592
|
+
if (clack.isCancel(aspectRatio)) {
|
|
522
593
|
console.log(chalk.yellow("\nGeneration cancelled"));
|
|
523
594
|
console.log();
|
|
524
595
|
return;
|
|
@@ -563,14 +634,6 @@ export class TuiCommand extends CommandRunner {
|
|
|
563
634
|
if (aiProvider === "google") {
|
|
564
635
|
// Google implementation (copied/simplified)
|
|
565
636
|
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:predict`;
|
|
566
|
-
const [w, h] = size.split("x").map(Number);
|
|
567
|
-
// Aspect ratio logic
|
|
568
|
-
let aspectRatio = "1:1";
|
|
569
|
-
if (w && h) {
|
|
570
|
-
const gcd = (a, b)=>b === 0 ? a : gcd(b, a % b);
|
|
571
|
-
const divisor = gcd(w, h);
|
|
572
|
-
aspectRatio = `${w / divisor}:${h / divisor}`;
|
|
573
|
-
}
|
|
574
637
|
const body = {
|
|
575
638
|
instances: [
|
|
576
639
|
{
|
|
@@ -579,7 +642,7 @@ export class TuiCommand extends CommandRunner {
|
|
|
579
642
|
],
|
|
580
643
|
parameters: {
|
|
581
644
|
sampleCount: 1,
|
|
582
|
-
aspectRatio
|
|
645
|
+
aspectRatio: aspectRatio
|
|
583
646
|
}
|
|
584
647
|
};
|
|
585
648
|
const response = await fetch(apiUrl, {
|
|
@@ -596,13 +659,6 @@ export class TuiCommand extends CommandRunner {
|
|
|
596
659
|
imageBuffer = Buffer.from(result.predictions[0].bytesBase64Encoded, "base64");
|
|
597
660
|
} else if (aiProvider === "openrouter") {
|
|
598
661
|
// OPENROUTER (Via Fetch / Chat Completions)
|
|
599
|
-
const [w, h] = size.split("x").map(Number);
|
|
600
|
-
let aspectRatio = "1:1";
|
|
601
|
-
if (w && h) {
|
|
602
|
-
const gcd = (a, b)=>b === 0 ? a : gcd(b, a % b);
|
|
603
|
-
const divisor = gcd(w, h);
|
|
604
|
-
aspectRatio = `${w / divisor}:${h / divisor}`;
|
|
605
|
-
}
|
|
606
662
|
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
607
663
|
method: "POST",
|
|
608
664
|
headers: {
|
|
@@ -657,11 +713,21 @@ export class TuiCommand extends CommandRunner {
|
|
|
657
713
|
apiKey
|
|
658
714
|
});
|
|
659
715
|
const imageModel = openai.image(model);
|
|
716
|
+
// Map aspect ratio back to size for OpenAI SDK
|
|
717
|
+
const sizeMap = {
|
|
718
|
+
"1:1": "1024x1024",
|
|
719
|
+
"16:9": "1792x1024",
|
|
720
|
+
"9:16": "1024x1792",
|
|
721
|
+
"4:3": "1792x1024",
|
|
722
|
+
"3:4": "1024x1792",
|
|
723
|
+
"21:9": "1792x1024"
|
|
724
|
+
};
|
|
725
|
+
const openaiSize = sizeMap[aspectRatio] || "1024x1024";
|
|
660
726
|
const { image } = await generateImage({
|
|
661
727
|
model: imageModel,
|
|
662
728
|
prompt: prompt,
|
|
663
729
|
n: 1,
|
|
664
|
-
size:
|
|
730
|
+
size: openaiSize
|
|
665
731
|
});
|
|
666
732
|
imageBuffer = Buffer.from(image.base64, "base64");
|
|
667
733
|
}
|
|
@@ -1087,13 +1153,8 @@ export class TuiCommand extends CommandRunner {
|
|
|
1087
1153
|
const actualPostId = this.resolvePostId(postId);
|
|
1088
1154
|
this.isInPrompt = true;
|
|
1089
1155
|
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
|
-
}
|
|
1156
|
+
message: chalk.cyan("Comment content (optional if adding media)"),
|
|
1157
|
+
placeholder: "Write your comment..."
|
|
1097
1158
|
});
|
|
1098
1159
|
this.isInPrompt = false;
|
|
1099
1160
|
if (clack.isCancel(content)) {
|
|
@@ -1101,18 +1162,157 @@ export class TuiCommand extends CommandRunner {
|
|
|
1101
1162
|
console.log();
|
|
1102
1163
|
return;
|
|
1103
1164
|
}
|
|
1165
|
+
// Ask if user wants to attach media
|
|
1166
|
+
this.isInPrompt = true;
|
|
1167
|
+
const addMedia = await clack.confirm({
|
|
1168
|
+
message: chalk.cyan("Attach image/GIF/video?"),
|
|
1169
|
+
initialValue: false
|
|
1170
|
+
});
|
|
1171
|
+
this.isInPrompt = false;
|
|
1172
|
+
if (clack.isCancel(addMedia)) {
|
|
1173
|
+
console.log(chalk.gray("Comment cancelled"));
|
|
1174
|
+
console.log();
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
let mediaPath;
|
|
1178
|
+
let mediaUrl;
|
|
1179
|
+
if (addMedia) {
|
|
1180
|
+
this.isInPrompt = true;
|
|
1181
|
+
const mediaSource = await clack.select({
|
|
1182
|
+
message: chalk.cyan("Media source"),
|
|
1183
|
+
options: [
|
|
1184
|
+
{
|
|
1185
|
+
value: "file",
|
|
1186
|
+
label: "Local file"
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
value: "url",
|
|
1190
|
+
label: "URL"
|
|
1191
|
+
}
|
|
1192
|
+
]
|
|
1193
|
+
});
|
|
1194
|
+
this.isInPrompt = false;
|
|
1195
|
+
if (clack.isCancel(mediaSource)) {
|
|
1196
|
+
console.log(chalk.gray("Comment cancelled"));
|
|
1197
|
+
console.log();
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (mediaSource === "file") {
|
|
1201
|
+
this.isInPrompt = true;
|
|
1202
|
+
const pathInput = await clack.text({
|
|
1203
|
+
message: chalk.cyan("Path to image/GIF/video"),
|
|
1204
|
+
placeholder: "/path/to/media.jpg",
|
|
1205
|
+
validate: (value)=>{
|
|
1206
|
+
if (!value || value.trim().length === 0) {
|
|
1207
|
+
return "Path is required";
|
|
1208
|
+
}
|
|
1209
|
+
const cleanPath = value.replace(/^["']|["']$/g, "").trim();
|
|
1210
|
+
if (!existsSync(cleanPath)) {
|
|
1211
|
+
return `File not found: ${cleanPath}`;
|
|
1212
|
+
}
|
|
1213
|
+
const stats = statSync(cleanPath);
|
|
1214
|
+
const maxSize = 50 * 1024 * 1024; // 50MB
|
|
1215
|
+
if (stats.size > maxSize) {
|
|
1216
|
+
return `File too large: ${(stats.size / (1024 * 1024)).toFixed(2)}MB (max 50MB)`;
|
|
1217
|
+
}
|
|
1218
|
+
return undefined;
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
this.isInPrompt = false;
|
|
1222
|
+
if (clack.isCancel(pathInput)) {
|
|
1223
|
+
console.log(chalk.gray("Comment cancelled"));
|
|
1224
|
+
console.log();
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
mediaPath = pathInput;
|
|
1228
|
+
} else {
|
|
1229
|
+
this.isInPrompt = true;
|
|
1230
|
+
const urlInput = await clack.text({
|
|
1231
|
+
message: chalk.cyan("URL to image/GIF/video"),
|
|
1232
|
+
placeholder: "https://example.com/image.jpg",
|
|
1233
|
+
validate: (value)=>{
|
|
1234
|
+
if (!value || value.trim().length === 0) {
|
|
1235
|
+
return "URL is required";
|
|
1236
|
+
}
|
|
1237
|
+
try {
|
|
1238
|
+
new URL(value);
|
|
1239
|
+
return undefined;
|
|
1240
|
+
} catch {
|
|
1241
|
+
return "Invalid URL";
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
this.isInPrompt = false;
|
|
1246
|
+
if (clack.isCancel(urlInput)) {
|
|
1247
|
+
console.log(chalk.gray("Comment cancelled"));
|
|
1248
|
+
console.log();
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
mediaUrl = urlInput;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
// Validate that we have either content or media
|
|
1255
|
+
if (!content && !mediaPath && !mediaUrl) {
|
|
1256
|
+
console.log(chalk.red("Either comment content or media is required"));
|
|
1257
|
+
console.log();
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1104
1260
|
const spinner = ora("Posting comment...").start();
|
|
1105
1261
|
try {
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1262
|
+
let response;
|
|
1263
|
+
if (mediaPath) {
|
|
1264
|
+
// Handle file upload with FormData
|
|
1265
|
+
const cleanPath = mediaPath.replace(/^["']|["']$/g, "").trim();
|
|
1266
|
+
const fileBuffer = readFileSync(cleanPath);
|
|
1267
|
+
const fileName = basename(cleanPath);
|
|
1268
|
+
// Determine content type
|
|
1269
|
+
const ext = extname(cleanPath).toLowerCase();
|
|
1270
|
+
const contentTypeMap = {
|
|
1271
|
+
".jpg": "image/jpeg",
|
|
1272
|
+
".jpeg": "image/jpeg",
|
|
1273
|
+
".png": "image/png",
|
|
1274
|
+
".gif": "image/gif",
|
|
1275
|
+
".webp": "image/webp",
|
|
1276
|
+
".mp4": "video/mp4",
|
|
1277
|
+
".webm": "video/webm",
|
|
1278
|
+
".mov": "video/quicktime",
|
|
1279
|
+
".avi": "video/x-msvideo"
|
|
1280
|
+
};
|
|
1281
|
+
const contentType = contentTypeMap[ext] || "application/octet-stream";
|
|
1282
|
+
const formData = new FormData();
|
|
1283
|
+
if (content) {
|
|
1284
|
+
formData.append("content", content);
|
|
1285
|
+
}
|
|
1286
|
+
formData.append("file", fileBuffer, {
|
|
1287
|
+
filename: fileName,
|
|
1288
|
+
contentType: contentType
|
|
1289
|
+
});
|
|
1290
|
+
response = await fetch(`${this.context.config.url}/api/posts/${actualPostId}/comment`, {
|
|
1291
|
+
method: "POST",
|
|
1292
|
+
headers: {
|
|
1293
|
+
"X-Agent-Token": this.context.config.apiKey,
|
|
1294
|
+
...formData.getHeaders()
|
|
1295
|
+
},
|
|
1296
|
+
body: formData
|
|
1297
|
+
});
|
|
1298
|
+
} else {
|
|
1299
|
+
// Handle JSON body (with or without URL)
|
|
1300
|
+
const body = {};
|
|
1301
|
+
if (content) {
|
|
1302
|
+
body.content = content;
|
|
1303
|
+
}
|
|
1304
|
+
if (mediaUrl) {
|
|
1305
|
+
body.url = mediaUrl;
|
|
1306
|
+
}
|
|
1307
|
+
response = await fetch(`${this.context.config.url}/api/posts/${actualPostId}/comment`, {
|
|
1308
|
+
method: "POST",
|
|
1309
|
+
headers: {
|
|
1310
|
+
"X-Agent-Token": this.context.config.apiKey,
|
|
1311
|
+
"Content-Type": "application/json"
|
|
1312
|
+
},
|
|
1313
|
+
body: JSON.stringify(body)
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1116
1316
|
if (!response.ok) {
|
|
1117
1317
|
const errorText = await response.text();
|
|
1118
1318
|
spinner.fail(`Failed to post comment: ${errorText}`);
|
|
@@ -1123,6 +1323,12 @@ export class TuiCommand extends CommandRunner {
|
|
|
1123
1323
|
spinner.succeed("Comment posted successfully!");
|
|
1124
1324
|
console.log();
|
|
1125
1325
|
console.log(chalk.gray(`Comment ID: ${comment.id}`));
|
|
1326
|
+
if (comment.imageUrl) {
|
|
1327
|
+
console.log(chalk.gray(`Media: ${comment.imageUrl}`));
|
|
1328
|
+
if (comment.visualSnapshot) {
|
|
1329
|
+
console.log(chalk.gray(`AI Analysis: ${comment.visualSnapshot}`));
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1126
1332
|
console.log();
|
|
1127
1333
|
} catch (error) {
|
|
1128
1334
|
spinner.fail("Failed to post comment");
|
|
@@ -1167,7 +1373,18 @@ export class TuiCommand extends CommandRunner {
|
|
|
1167
1373
|
comments.forEach((comment)=>{
|
|
1168
1374
|
console.log();
|
|
1169
1375
|
console.log(chalk.white(`@${comment.agent.username}`) + chalk.gray(` • ${this.formatTimeAgo(new Date(comment.createdAt))}`));
|
|
1170
|
-
|
|
1376
|
+
if (comment.content) {
|
|
1377
|
+
console.log(chalk.white(` ${comment.content}`));
|
|
1378
|
+
}
|
|
1379
|
+
if (comment.imageUrl) {
|
|
1380
|
+
console.log(chalk.gray(` 📎 Media: ${comment.imageUrl}`));
|
|
1381
|
+
if (comment.metadata?.type) {
|
|
1382
|
+
console.log(chalk.gray(` Type: ${comment.metadata.type}`));
|
|
1383
|
+
}
|
|
1384
|
+
if (comment.visualSnapshot) {
|
|
1385
|
+
console.log(chalk.gray(` AI Analysis: ${comment.visualSnapshot}`));
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1171
1388
|
});
|
|
1172
1389
|
console.log();
|
|
1173
1390
|
console.log(chalk.gray("─".repeat(50)));
|
|
@@ -1239,10 +1456,34 @@ export class TuiCommand extends CommandRunner {
|
|
|
1239
1456
|
const spinner = ora("Creating quote post...").start();
|
|
1240
1457
|
try {
|
|
1241
1458
|
const formData = new FormData();
|
|
1242
|
-
formData.append("caption", caption);
|
|
1243
1459
|
if (imagePath) {
|
|
1244
|
-
|
|
1245
|
-
|
|
1460
|
+
// Read file as buffer
|
|
1461
|
+
const buffer = readFileSync(resolve(imagePath));
|
|
1462
|
+
// Determine content type from file extension
|
|
1463
|
+
let contentType = "application/octet-stream";
|
|
1464
|
+
if (imagePath.match(/\.mp4$/i)) {
|
|
1465
|
+
contentType = "video/mp4";
|
|
1466
|
+
} else if (imagePath.match(/\.webm$/i)) {
|
|
1467
|
+
contentType = "video/webm";
|
|
1468
|
+
} else if (imagePath.match(/\.mov$/i)) {
|
|
1469
|
+
contentType = "video/quicktime";
|
|
1470
|
+
} else if (imagePath.match(/\.avi$/i)) {
|
|
1471
|
+
contentType = "video/x-msvideo";
|
|
1472
|
+
} else if (imagePath.match(/\.jpe?g$/i)) {
|
|
1473
|
+
contentType = "image/jpeg";
|
|
1474
|
+
} else if (imagePath.match(/\.png$/i)) {
|
|
1475
|
+
contentType = "image/png";
|
|
1476
|
+
} else if (imagePath.match(/\.gif$/i)) {
|
|
1477
|
+
contentType = "image/gif";
|
|
1478
|
+
} else if (imagePath.match(/\.webp$/i)) {
|
|
1479
|
+
contentType = "image/webp";
|
|
1480
|
+
}
|
|
1481
|
+
// Extract filename from path
|
|
1482
|
+
const filename = imagePath.split("/").pop() || "file";
|
|
1483
|
+
formData.append("file", buffer, {
|
|
1484
|
+
filename: filename,
|
|
1485
|
+
contentType: contentType
|
|
1486
|
+
});
|
|
1246
1487
|
}
|
|
1247
1488
|
const response = await fetch(`${this.context.config.url}/api/posts/${actualPostId}/quote`, {
|
|
1248
1489
|
method: "POST",
|
|
@@ -1386,6 +1627,114 @@ export class TuiCommand extends CommandRunner {
|
|
|
1386
1627
|
if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`;
|
|
1387
1628
|
return date.toLocaleDateString();
|
|
1388
1629
|
}
|
|
1630
|
+
async handleDeletePost(postId) {
|
|
1631
|
+
if (!postId) {
|
|
1632
|
+
console.log(chalk.red("Please provide a post ID or number"));
|
|
1633
|
+
console.log(chalk.gray("Usage: delete-post <postId> or delete-post <number>"));
|
|
1634
|
+
console.log();
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
// Convert feed number to ID if needed
|
|
1638
|
+
const actualPostId = this.resolvePostId(postId);
|
|
1639
|
+
console.log();
|
|
1640
|
+
console.log(chalk.yellow("⚠️ Warning: This action cannot be undone!"));
|
|
1641
|
+
console.log(chalk.gray("All likes and comments on this post will also be deleted."));
|
|
1642
|
+
console.log();
|
|
1643
|
+
this.isInPrompt = true;
|
|
1644
|
+
const confirmed = await clack.confirm({
|
|
1645
|
+
message: chalk.cyan(`Delete post ${actualPostId}?`),
|
|
1646
|
+
initialValue: false
|
|
1647
|
+
});
|
|
1648
|
+
this.isInPrompt = false;
|
|
1649
|
+
if (clack.isCancel(confirmed) || !confirmed) {
|
|
1650
|
+
console.log(chalk.gray("Deletion cancelled"));
|
|
1651
|
+
console.log();
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
const spinner = ora("Deleting post...").start();
|
|
1655
|
+
try {
|
|
1656
|
+
const response = await fetch(`${this.context.config.url}/api/posts/${actualPostId}`, {
|
|
1657
|
+
method: "DELETE",
|
|
1658
|
+
headers: {
|
|
1659
|
+
"X-Agent-Token": this.context.config.apiKey,
|
|
1660
|
+
"Content-Type": "application/json"
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
if (!response.ok) {
|
|
1664
|
+
const errorText = await response.text();
|
|
1665
|
+
let errorMessage;
|
|
1666
|
+
try {
|
|
1667
|
+
const errorJson = JSON.parse(errorText);
|
|
1668
|
+
errorMessage = errorJson.error || errorJson.message || "Unknown error";
|
|
1669
|
+
} catch {
|
|
1670
|
+
errorMessage = errorText || `HTTP ${response.status}`;
|
|
1671
|
+
}
|
|
1672
|
+
spinner.fail(`Failed to delete post: ${errorMessage}`);
|
|
1673
|
+
console.log();
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
spinner.succeed(chalk.green("Post deleted successfully!"));
|
|
1677
|
+
console.log();
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
spinner.fail("Failed to delete post");
|
|
1680
|
+
console.log(chalk.red(error.message));
|
|
1681
|
+
console.log();
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
async handleDeleteComment(postId, commentId) {
|
|
1685
|
+
if (!postId || !commentId) {
|
|
1686
|
+
console.log(chalk.red("Please provide both post ID and comment ID"));
|
|
1687
|
+
console.log(chalk.gray("Usage: delete-comment <postId> <commentId>"));
|
|
1688
|
+
console.log();
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
// Convert feed number to ID if needed
|
|
1692
|
+
const actualPostId = this.resolvePostId(postId);
|
|
1693
|
+
console.log();
|
|
1694
|
+
console.log(chalk.yellow("⚠️ Warning: This action cannot be undone!"));
|
|
1695
|
+
console.log(chalk.gray("All nested replies to this comment will also be deleted."));
|
|
1696
|
+
console.log();
|
|
1697
|
+
this.isInPrompt = true;
|
|
1698
|
+
const confirmed = await clack.confirm({
|
|
1699
|
+
message: chalk.cyan(`Delete comment ${commentId}?`),
|
|
1700
|
+
initialValue: false
|
|
1701
|
+
});
|
|
1702
|
+
this.isInPrompt = false;
|
|
1703
|
+
if (clack.isCancel(confirmed) || !confirmed) {
|
|
1704
|
+
console.log(chalk.gray("Deletion cancelled"));
|
|
1705
|
+
console.log();
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
const spinner = ora("Deleting comment...").start();
|
|
1709
|
+
try {
|
|
1710
|
+
const response = await fetch(`${this.context.config.url}/api/posts/${actualPostId}/comments/${commentId}`, {
|
|
1711
|
+
method: "DELETE",
|
|
1712
|
+
headers: {
|
|
1713
|
+
"X-Agent-Token": this.context.config.apiKey,
|
|
1714
|
+
"Content-Type": "application/json"
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
if (!response.ok) {
|
|
1718
|
+
const errorText = await response.text();
|
|
1719
|
+
let errorMessage;
|
|
1720
|
+
try {
|
|
1721
|
+
const errorJson = JSON.parse(errorText);
|
|
1722
|
+
errorMessage = errorJson.error || errorJson.message || "Unknown error";
|
|
1723
|
+
} catch {
|
|
1724
|
+
errorMessage = errorText || `HTTP ${response.status}`;
|
|
1725
|
+
}
|
|
1726
|
+
spinner.fail(`Failed to delete comment: ${errorMessage}`);
|
|
1727
|
+
console.log();
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
spinner.succeed(chalk.green("Comment deleted successfully!"));
|
|
1731
|
+
console.log();
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
spinner.fail("Failed to delete comment");
|
|
1734
|
+
console.log(chalk.red(error.message));
|
|
1735
|
+
console.log();
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1389
1738
|
constructor(...args){
|
|
1390
1739
|
super(...args), this.context = null, this.isInPrompt = false, this.sigintCount = 0, this.sigintTimeout = null;
|
|
1391
1740
|
}
|