38
38
import json
39
39
import logging
40
40
import os
41
+ import sys
41
42
import time
42
43
from dataclasses import asdict
43
44
from datetime import datetime
@@ -358,7 +359,7 @@ def get_conversation_thread(client, tweet_id_or_conversation_id, max_pages=3, ma
358
359
"created_at" : t .created_at .isoformat (),
359
360
"depth" : depth , # Add depth information for UI indentation
360
361
"replied_to_id" : reply_structure .get (t .id ), # Which tweet this is replying to
361
- "public_metrics" : t .public_metrics if hasattr (t , "public_metrics" ) else {},
362
+ "public_metrics" : ( t .public_metrics if hasattr (t , "public_metrics" ) else {}) ,
362
363
# Include referenced tweets if available
363
364
"referenced_tweets" : [],
364
365
}
@@ -383,7 +384,9 @@ def get_conversation_thread(client, tweet_id_or_conversation_id, max_pages=3, ma
383
384
"type" : ref .type ,
384
385
"id" : ref .id ,
385
386
"text" : ref_tweet .text if ref_tweet else "Unavailable" ,
386
- "author" : author .username if ref_tweet and ref_tweet .author_id in all_users else "Unknown" ,
387
+ "author" : (
388
+ author .username if ref_tweet and ref_tweet .author_id in all_users else "Unknown"
389
+ ),
387
390
}
388
391
)
389
392
@@ -426,6 +429,51 @@ def move_draft(path: Path, new_status: str) -> Path:
426
429
return new_path
427
430
428
431
432
+ def find_draft (draft_id : str , status : Optional [str ] = None , show_error : bool = True ) -> Optional [Path ]:
433
+ """Find a draft by ID or path.
434
+
435
+ Args:
436
+ draft_id: Either a simple ID, filename, or full path
437
+ status: Optional status to look in specific directory
438
+ If None, looks in all status directories
439
+ show_error: Whether to print error message if not found
440
+
441
+ Returns:
442
+ Path to draft if found, None otherwise
443
+ """
444
+ status_dirs = {
445
+ "new" : NEW_DIR ,
446
+ "review" : REVIEW_DIR ,
447
+ "approved" : APPROVED_DIR ,
448
+ "posted" : POSTED_DIR ,
449
+ "rejected" : REJECTED_DIR ,
450
+ }
451
+
452
+ # Handle full paths
453
+ if "/" in draft_id :
454
+ draft_path = Path (draft_id )
455
+ if not draft_path .exists () and not draft_path .suffix :
456
+ draft_path = draft_path .with_suffix (".yml" )
457
+ if draft_path .exists ():
458
+ return draft_path
459
+ else :
460
+ # Search in specified directories
461
+ search_dirs = [status_dirs [status ]] if status else status_dirs .values ()
462
+ for dir in search_dirs :
463
+ for path in [dir / draft_id , (dir / draft_id ).with_suffix (".yml" )] + list (dir .glob (f"*{ draft_id } *.yml" )):
464
+ if path .exists ():
465
+ return path
466
+
467
+ if show_error :
468
+ status_msg = f" in { status } directory" if status else ""
469
+ console .print (f"[red]No draft found{ status_msg } : { draft_id } " )
470
+ console .print (
471
+ "[yellow]ID can be: simple ID, filename, or full path (e.g., tweet_20250419, reply_*.yml, tweets/new/*.yml)"
472
+ )
473
+
474
+ return None
475
+
476
+
429
477
def list_drafts (status : str ) -> List [Path ]:
430
478
"""List all drafts in a status directory"""
431
479
status_dirs = {
@@ -448,7 +496,7 @@ def list_drafts(status: str) -> List[Path]:
448
496
default = os .getenv ("MODEL" , "anthropic/claude-3-5-sonnet-20241022" ),
449
497
help = "Model to use for LLM operations" ,
450
498
)
451
- def cli (model : str ) :
499
+ def cli (model : str | None = None ) -> None :
452
500
"""Twitter Workflow Manager"""
453
501
init_gptme (model = model , interactive = False , tool_allowlist = [])
454
502
@@ -561,15 +609,11 @@ def review(auto_approve: bool, show_context: bool, dry_run: bool) -> None:
561
609
@cli .command ()
562
610
@click .argument ("draft_id" )
563
611
def approve (draft_id : str ) -> None :
564
- """Approve a draft tweet by ID"""
565
- # Find the draft by ID
566
- draft_files = list (NEW_DIR .glob (f"{ draft_id } *.yml" ))
567
-
568
- if not draft_files :
569
- console .print (f"[red]No draft found with ID: { draft_id } " )
612
+ """Approve a draft tweet by ID or path"""
613
+ draft_path = find_draft (draft_id , "new" )
614
+ if not draft_path :
570
615
return
571
616
572
- draft_path = draft_files [0 ]
573
617
new_path = move_draft (draft_path , "approved" )
574
618
console .print (f"[green]Draft approved: { draft_path .name } → { new_path .name } " )
575
619
@@ -578,19 +622,11 @@ def approve(draft_id: str) -> None:
578
622
@click .argument ("draft_id" )
579
623
def reject (draft_id : str ) -> None :
580
624
"""Reject a draft tweet by ID (works on both new and approved drafts)"""
581
- # First try to find the draft in the NEW_DIR
582
- draft_files = list (NEW_DIR .glob (f"{ draft_id } *.yml" ))
583
-
584
- # If not found, try APPROVED_DIR
585
- if not draft_files :
586
- draft_files = list (APPROVED_DIR .glob (f"{ draft_id } *.yml" ))
587
-
588
- # Still not found
589
- if not draft_files :
590
- console .print (f"[red]No draft found with ID: { draft_id } in new or approved directories" )
625
+ # Try to find draft in either new or approved directories
626
+ draft_path = find_draft (draft_id , "new" , show_error = False ) or find_draft (draft_id , "approved" )
627
+ if not draft_path :
591
628
return
592
629
593
- draft_path = draft_files [0 ]
594
630
new_path = move_draft (draft_path , "rejected" )
595
631
console .print (f"[red]Draft rejected: { draft_path .name } → { new_path .name } " )
596
632
@@ -600,21 +636,12 @@ def reject(draft_id: str) -> None:
600
636
@click .argument ("new_text" )
601
637
def edit (draft_id : str , new_text : str ) -> None :
602
638
"""Edit a draft tweet by ID (works on both new and approved drafts)"""
603
- # First try to find the draft in the NEW_DIR
604
- draft_files = list (NEW_DIR .glob (f"{ draft_id } *.yml" ))
605
-
606
- # If not found, try APPROVED_DIR
607
- if not draft_files :
608
- draft_files = list (APPROVED_DIR .glob (f"{ draft_id } *.yml" ))
609
-
610
- # Still not found
611
- if not draft_files :
612
- console .print (f"[red]No draft found with ID: { draft_id } in new or approved directories" )
639
+ # Try to find draft in either new or approved directories
640
+ draft_path = find_draft (draft_id , "new" , show_error = False ) or find_draft (draft_id , "approved" )
641
+ if not draft_path :
613
642
return
614
643
615
- draft_path = draft_files [0 ]
616
644
draft = TweetDraft .load (draft_path )
617
-
618
645
console .print (f"[cyan]Original text: { draft .text } " )
619
646
draft .text = new_text
620
647
draft .save (draft_path )
@@ -625,17 +652,15 @@ def edit(draft_id: str, new_text: str) -> None:
625
652
@cli .command ()
626
653
@click .option ("--dry-run" , is_flag = True , help = "Don't actually post tweets" )
627
654
@click .option ("--yes" , "-y" , is_flag = True , help = "Skip confirmation prompt" )
628
- @click .option ("--draft-id" , help = "Post a specific draft by ID" )
655
+ @click .option ("--draft-id" , help = "Post a specific draft by ID or path " )
629
656
def post (dry_run : bool , yes : bool , draft_id : Optional [str ] = None ) -> None :
630
657
"""Post approved tweets"""
631
-
632
658
# If a specific draft ID is provided, find only that draft
633
659
if draft_id :
634
- draft_files = list (APPROVED_DIR .glob (f"*{ draft_id } *.yml" ))
635
- if not draft_files :
636
- console .print (f"[red]No approved draft found with ID: { draft_id } " )
660
+ draft_path = find_draft (draft_id , "approved" )
661
+ if not draft_path :
637
662
return
638
- drafts = draft_files
663
+ drafts = [ draft_path ]
639
664
else :
640
665
drafts = list_drafts ("approved" )
641
666
@@ -799,8 +824,8 @@ def process_timeline_tweets(
799
824
in_reply_to = tweet .id if response .type == "reply" else None ,
800
825
context = {
801
826
"original_tweet" : tweet_data ,
802
- "evaluation" : asdict (eval_result ) if eval_result is not None else None , # Convert to dict
803
- "response_metadata" : asdict (response ) if response is not None else None , # Convert to dict
827
+ "evaluation" : ( asdict (eval_result ) if eval_result is not None else None ) , # Convert to dict
828
+ "response_metadata" : ( asdict (response ) if response is not None else None ) , # Convert to dict
804
829
},
805
830
)
806
831
@@ -1155,7 +1180,7 @@ def auto(
1155
1180
1156
1181
if needs_review_count > 0 :
1157
1182
console .print ("\n [yellow]Run the following command to review pending drafts:[/yellow]" )
1158
- console .print ("[blue]./twitter-cli.py workflow review[/blue]" )
1183
+ console .print (f "[blue]{ sys . argv [ 0 ] } review[/blue]" )
1159
1184
1160
1185
1161
1186
if __name__ == "__main__" :
0 commit comments