Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/just-cameron/central. Autonomous AI building collective intelligence on ATProtocol. The central nod central.comind.network
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165"""Thread Builder ToolPublishes multi-post threads to ATProtocol."""
import asyncioimport sysimport argparseimport httpxfrom pathlib import Pathfrom rich.console import Console
sys.path.insert(0, str(Path(__file__).parent.parent))from tools.agent import ComindAgent, PostResult
console = Console()
async def get_reply_context(uri: str) -> dict | None: """ Get the root and parent context for a reply target. Returns {'root': {...}, 'parent': {...}} or None if failed. """ async with httpx.AsyncClient() as client: try: # 1. Get the post to find its CID and if it's a reply itself resp = await client.get( "https://public.api.bsky.app/xrpc/app.bsky.feed.getPosts", params={"uris": uri} ) if resp.status_code != 200: console.print(f"[red]Error fetching reply target: {resp.status_code}[/red]") return None posts = resp.json().get("posts", []) if not posts: console.print("[red]Reply target not found[/red]") return None target = posts[0] target_cid = target.get("cid") target_uri = target.get("uri") record = target.get("record", {}) # Determine root if "reply" in record: # Target is already a reply, use its root root = record["reply"]["root"] else: # Target is a top-level post, it becomes the root root = {"uri": target_uri, "cid": target_cid} # Parent is always the target we are replying to parent = {"uri": target_uri, "cid": target_cid} return {"root": root, "parent": parent} except Exception as e: console.print(f"[red]Error determining reply context: {e}[/red]") return None
async def publish_thread(posts: list[str], reply_to_uri: str = None): """ Publish a list of posts as a thread. Args: posts: List of post text strings reply_to_uri: Optional URI of a post to reply to (starts thread as reply) """ if not posts: console.print("[yellow]No posts provided.[/yellow]") return
# Validate lengths for i, p in enumerate(posts): if len(p) > 300: # Rough check, agent.py does strict grapheme check console.print(f"[red]Post {i+1} is too long ({len(p)} chars). Limit is ~300.[/red]") return
async with ComindAgent() as agent: root_ref = None parent_ref = None # Initialize context if replying if reply_to_uri: console.print(f"[bold]Fetching context for reply target:[/bold] {reply_to_uri}") context = await get_reply_context(reply_to_uri) if not context: return root_ref = context["root"] parent_ref = context["parent"] console.print(f"[dim]Root: {root_ref['uri']}[/dim]") console.print(f"[dim]Parent: {parent_ref['uri']}[/dim]")
print() for i, text in enumerate(posts): console.print(f"[bold cyan]Posting {i+1}/{len(posts)}...[/bold cyan]") # Construct reply ref if needed reply_ref = None if root_ref and parent_ref: reply_ref = {"root": root_ref, "parent": parent_ref} # Create the post with retry for transient failures result = await agent.create_post_with_retry(text, reply_to=reply_ref) if not result.success: console.print(f"[red]Failed to publish post {i+1}[/red]") console.print(f"[red] Error type: {result.error_type}[/red]") console.print(f"[red] Message: {result.error_message}[/red]") console.print(f"[red] Retryable: {result.retryable}[/red]") console.print("[red]Aborting thread.[/red]") break # Update refs for next post new_ref = {"uri": result.uri, "cid": result.cid} if i == 0 and not reply_to_uri: # First post of a new thread becomes the root root_ref = new_ref # Always update parent to be the just-created post parent_ref = new_ref console.print(f"[green]Published:[/green] {result.uri}") console.print(f"[dim]{text[:50]}...[/dim]") # Small delay to ensure order and avoid rate limits await asyncio.sleep(0.5)
console.print("\n[bold green]Thread complete.[/bold green]")
if __name__ == "__main__": parser = argparse.ArgumentParser(description="Publish a thread to Bluesky") parser.add_argument("posts", nargs="*", help="List of posts to publish") parser.add_argument("--file", "-f", help="File containing posts (separated by '---' on a new line)") parser.add_argument("--reply-to", "-r", help="URI of a post to reply to") args = parser.parse_args() posts = [] # 1. Load from file if args.file: try: with open(args.file, "r") as f: content = f.read() # Split by separator parts = content.split("\n---\n") posts.extend([p.strip() for p in parts if p.strip()]) except Exception as e: console.print(f"[red]Error reading file: {e}[/red]") sys.exit(1) # 2. Append/Load CLI args if args.posts: posts.extend(args.posts) if not posts: console.print("[red]No posts provided. Use args or --file.[/red]") sys.exit(1) asyncio.run(publish_thread(posts, reply_to_uri=args.reply_to))