diff --git a/src/void_tools/cli.py b/src/void_tools/cli.py index 7087493..d3b5407 100644 --- a/src/void_tools/cli.py +++ b/src/void_tools/cli.py @@ -705,6 +705,62 @@ def cmd_annotate(args): print(f"Error annotating {target_uri}: {e}", file=sys.stderr) sys.exit(1) +def cmd_ingest_thread(args): + client = get_client() + try: + response = client.app.bsky.feed.get_post_thread({'uri': args.uri, 'depth': args.depth, 'parent_height': 100}) + thread = response.thread + + agent_id = os.getenv("AGENT_ID") + if not agent_id: + print("Error: AGENT_ID not set in environment.", file=sys.stderr) + sys.exit(1) + + memfs_threads_dir = os.path.expanduser(f"~/.letta/agents/{agent_id}/memory/threads") + os.makedirs(memfs_threads_dir, exist_ok=True) + + # Extract post ID from URI for filename + post_id = args.uri.split('/')[-1] + timestamp = datetime.datetime.now(datetime.UTC).strftime('%Y%m%d_%H%M%S') + filename = os.path.join(memfs_threads_dir, f"thread_{post_id}_{timestamp}.md") + + def format_thread_node(node, level=0): + content = "" + indent = " " * level + if hasattr(node, 'post'): + post = node.post + author = post.author.handle + text = post.record.text.replace('\n', '\n' + indent + '> ') + content += f"{indent}- **@{author}**: {text}\n" + + if hasattr(node, 'replies') and node.replies: + for reply in node.replies: + content += format_thread_node(reply, level + 1) + return content + + # Handle parent traversal first to get full context + full_content = f"# Thread Ingestion: {args.uri}\n\n" + + # We need to trace up to the root if parents exist + parents = [] + curr = thread + while hasattr(curr, 'parent') and curr.parent: + parents.insert(0, curr.parent) + curr = curr.parent + + for p in parents: + full_content += format_thread_node(p, 0) + + full_content += format_thread_node(thread, len(parents)) + + with open(filename, 'w') as f: + f.write(full_content) + + print(f"Successfully ingested thread into {filename}") + + except Exception as e: + print(f"Error ingesting thread: {e}", file=sys.stderr) + def main(): parser = argparse.ArgumentParser(description="Void ATProto CLI Tools") subparsers = parser.add_subparsers(dest="command", required=True) @@ -727,6 +783,10 @@ def main(): search_parser.add_argument("query", help="Search query string") search_parser.add_argument("--limit", type=int, default=10, help="Number of posts to return") + # Ingest Thread + ingest_parser = subparsers.add_parser("ingest-thread", help="Download an entire post thread and save it to MemFS") + ingest_parser.add_argument("uri", help="The AT-URI of the root or target post") + ingest_parser.add_argument("--depth", type=int, default=10, help="Maximum depth of replies to fetch") # Profile profile_parser = subparsers.add_parser("profile", help="Manage profile") @@ -761,6 +821,8 @@ def main(): cmd_post(args) elif args.command == "search": cmd_search(args) + elif args.command == "ingest-thread": + cmd_ingest_thread(args) elif args.command == "profile": cmd_profile(args) elif args.command == "annotate":