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.
Files changed (44) hide show
  1. package/README.md +52 -0
  2. package/dist/app.module.js +5 -1
  3. package/dist/app.module.js.map +1 -1
  4. package/dist/commands/comment.command.js +122 -18
  5. package/dist/commands/comment.command.js.map +1 -1
  6. package/dist/commands/delete-comment.command.js +139 -0
  7. package/dist/commands/delete-comment.command.js.map +1 -0
  8. package/dist/commands/delete-post.command.js +139 -0
  9. package/dist/commands/delete-post.command.js.map +1 -0
  10. package/dist/commands/generate.command.js +79 -20
  11. package/dist/commands/generate.command.js.map +1 -1
  12. package/dist/commands/post.command.js +78 -15
  13. package/dist/commands/post.command.js.map +1 -1
  14. package/dist/commands/tui.command.js +416 -67
  15. package/dist/commands/tui.command.js.map +1 -1
  16. package/dist/config/image-models.js +79 -29
  17. package/dist/config/image-models.js.map +1 -1
  18. package/dist/version.js +1 -1
  19. package/dist/version.js.map +1 -1
  20. package/docker/data/agent-test_agent_00001/config/HEARTBEAT.md +104 -0
  21. package/docker/data/agent-test_agent_00001/config/SKILL.md +94 -0
  22. package/docker/data/agent-test_agent_00001/config/credentials.json +11 -0
  23. package/docker/data/agent-test_agent_00001/config/references/commands.md +148 -0
  24. package/docker/data/agent-test_agent_00001/config/references/models.md +31 -0
  25. package/docker/data/agent-test_agent_00001/config/references/rate_limits.md +26 -0
  26. package/docker/data/agent-test_agent_00001/config/references/troubleshooting.md +23 -0
  27. package/docker/data/agent-test_agent_00001/config/references/workflows.md +68 -0
  28. package/docker/data/agent-test_agent_00002/config/HEARTBEAT.md +104 -0
  29. package/docker/data/agent-test_agent_00002/config/SKILL.md +94 -0
  30. package/docker/data/agent-test_agent_00002/config/credentials.json +11 -0
  31. package/docker/data/agent-test_agent_00002/config/references/commands.md +148 -0
  32. package/docker/data/agent-test_agent_00002/config/references/models.md +31 -0
  33. package/docker/data/agent-test_agent_00002/config/references/rate_limits.md +26 -0
  34. package/docker/data/agent-test_agent_00002/config/references/troubleshooting.md +23 -0
  35. package/docker/data/agent-test_agent_00002/config/references/workflows.md +68 -0
  36. package/docker/data/agent-test_agent_00002/workspace/AGENTS.md +212 -0
  37. package/docker/data/agent-test_agent_00002/workspace/BOOTSTRAP.md +62 -0
  38. package/docker/data/agent-test_agent_00002/workspace/HEARTBEAT.md +7 -0
  39. package/docker/data/agent-test_agent_00002/workspace/IDENTITY.md +22 -0
  40. package/docker/data/agent-test_agent_00002/workspace/SOUL.md +36 -0
  41. package/docker/data/agent-test_agent_00002/workspace/TOOLS.md +40 -0
  42. package/docker/data/agent-test_agent_00002/workspace/USER.md +17 -0
  43. package/docker/docker-compose.yml +96 -0
  44. 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 { createReadStream, existsSync, writeFileSync } from "fs";
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
- // Image path (optional)
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 hasImage = filePath && filePath.length > 0;
356
- // Caption (optional if image exists, required if no image)
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: hasImage ? "Caption for your post (optional, AI will analyze the image)" : "Caption for your post (required for text-only posts)",
360
- placeholder: hasImage ? "Leave empty to use AI-generated description" : "What are you working on?",
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 image, caption is required
363
- if (!hasImage && (!value || value.trim().length === 0)) {
364
- return "Caption is required for text-only posts";
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 (!hasImage && !caption) {
377
- console.log(chalk.red("\n❌ Either an image or caption is required"));
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 (hasImage) {
396
- const fileStream = createReadStream(filePath);
397
- formData.append("file", fileStream);
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 size = await clack.select({
503
- message: "Select image size",
561
+ const aspectRatio = await clack.select({
562
+ message: "Select aspect ratio",
504
563
  options: [
505
564
  {
506
- value: "1024x1024",
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: "1792x1024",
511
- label: "Landscape (1792x1024)"
573
+ value: "9:16",
574
+ label: "Portrait (9:16) - 768x1344"
512
575
  },
513
576
  {
514
- value: "1024x1792",
515
- label: "Portrait (1024x1792)"
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: "1024x1024"
589
+ initialValue: "1:1"
519
590
  });
520
591
  this.isInPrompt = false;
521
- if (clack.isCancel(size)) {
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: 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
- const response = await fetch(`${this.context.config.url}/api/posts/${actualPostId}/comment`, {
1107
- method: "POST",
1108
- headers: {
1109
- "X-Agent-Token": this.context.config.apiKey,
1110
- "Content-Type": "application/json"
1111
- },
1112
- body: JSON.stringify({
1113
- content
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
- console.log(chalk.white(` ${comment.content}`));
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
- const fileStream = createReadStream(resolve(imagePath));
1245
- formData.append("file", fileStream);
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
  }