From a6b43cd62319f5c0402878747dab37410fea2388 Mon Sep 17 00:00:00 2001 From: Mark Randall Havens Date: Fri, 13 Mar 2026 03:37:57 +0000 Subject: [PATCH] Add output options: save to S3 and GitHub New CLI options: --save-s3 BUCKET/PATH Save manuscript to S3 --save-s3-endpoint URL S3 endpoint for MinIO/DO Spaces --save-repo OWNER/REPO Commit to GitHub repo --save-branch NAME GitHub branch (default: main) --save-commit-msg MSG Commit message Full pipeline example: opus generate --repo owner/notes --save-repo owner/manuscripts --save-branch develop --- README.md | 26 ++++++- opus_orchestrator/cli.py | 149 ++++++++++++++++++++++++++++++++++----- 2 files changed, 155 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index fffaa73..dd00f24 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,14 @@ opus --api-url http://localhost:8000 generate --concept "Your idea" | **API Client** | Client mode for remote servers | ✅ | | **Python Module** | Import as library | ✅ | +### Output Options + +| Destination | CLI Flag | Description | +|------------|----------|-------------| +| **Local File** | `--output FILE` | Save to local filesystem | +| **S3/MinIO** | `--save-s3 BUCKET/PATH` | Upload to S3-compatible storage | +| **GitHub** | `--save-repo OWNER/REPO` | Commit to GitHub repository | + --- ## 🚀 Usage @@ -97,13 +105,27 @@ opus --api-url http://localhost:8000 generate --concept "Your idea" ### CLI Commands ```bash -# Generate manuscript (local) +# Generate manuscript (local, save to file) opus generate --concept "Your story idea" --framework snowflake --words 5000 # Generate from GitHub opus generate --repo owner/repo --framework hero-journey --words 80000 -# Generate from S3/MinIO +# Generate and save to S3/MinIO +opus generate --concept "..." --save-s3 my-bucket/manuscripts/ +opus generate --concept "..." --save-s3 my-bucket/path/ --save-s3-endpoint https://nyc3.digitaloceanspaces.com + +# Generate and save to GitHub repo +opus generate --concept "..." --save-repo owner/my-manuscripts +opus generate --concept "..." --save-repo owner/my-manuscripts --save-branch develop --save-commit-msg "New story draft" + +# Generate from S3, save to GitHub +opus generate --repo owner/repo --save-repo owner/output-repo + +# Generate from S3, save to different S3 bucket +opus ingest-s3 --bucket input-bucket --prefix notes/ | opus generate --save-s3 output-bucket/ + +# Ingest from S3/MinIO opus ingest-s3 --bucket my-bucket --prefix notes/ --output content.txt # Start API server diff --git a/opus_orchestrator/cli.py b/opus_orchestrator/cli.py index 02e987e..d79cfb8 100644 --- a/opus_orchestrator/cli.py +++ b/opus_orchestrator/cli.py @@ -246,7 +246,28 @@ Examples: ) gen_parser.add_argument( "--output", "-o", - help="Output file path", + help="Output file path (local)", + ) + gen_parser.add_argument( + "--save-s3", + help="Save to S3 bucket (bucket/path format)", + ) + gen_parser.add_argument( + "--save-s3-endpoint", + help="S3 endpoint URL (for MinIO, DO Spaces, etc.)", + ) + gen_parser.add_argument( + "--save-repo", + help="Save to GitHub repo (owner/repo format)", + ) + gen_parser.add_argument( + "--save-branch", + default="main", + help="GitHub branch to commit to (default: main)", + ) + gen_parser.add_argument( + "--save-commit-msg", + help="Commit message for GitHub save", ) gen_parser.add_argument( "--use-crewai", @@ -549,29 +570,121 @@ async def run_generate(args: argparse.Namespace) -> int: manuscript = result.get("manuscript", str(result)) - # Save output - output_path = args.output - if not output_path: - from datetime import datetime - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - output_path = f"opus_manuscript_{timestamp}.md" - - with open(output_path, "w") as f: - f.write(f"# Opus Generated Manuscript\n\n") - f.write(f"Framework: {args.framework}\n") - f.write(f"Genre: {args.genre}\n") - f.write(f"Type: {args.book_type}\n") - f.write(f"Chapters: {args.chapters}\n") - f.write(f"Target Words: {args.words:,}\n\n") - f.write(f"---\n\n") - f.write(manuscript) + # Build full manuscript content with metadata + manuscript_content = f"""# Opus Generated Manuscript + +Framework: {args.framework} +Genre: {args.genre} +Type: {args.book_type} +Chapters: {args.chapters} +Target Words: {args.words:,} + +--- + +{manuscript} +""" word_count = len(manuscript.split()) + # Save to local file + output_path = args.output + if output_path or args.save_s3 or args.save_repo: + # Determine filename + if not output_path: + from datetime import datetime + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + output_path = f"opus_manuscript_{timestamp}.md" + + with open(output_path, "w") as f: + f.write(manuscript_content) + + print(f" 💾 Saved locally: {output_path}") + + # Save to S3 + if args.save_s3: + from opus_orchestrator import S3Ingestor + + bucket, key = args.save_s3.split("/", 1) if "/" in args.save_s3 else (args.save_s3, "") + if not key: + from datetime import datetime + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + key = f"manuscripts/opus_{timestamp}.md" + + print(f" 🪣 Saving to S3: {bucket}/{key}") + + s3 = S3Ingestor(endpoint_url=args.save_s3_endpoint, bucket=bucket) + + # Upload the manuscript content + import io + from botocore.config import Config + + s3.s3_client.put_object( + Bucket=bucket, + Key=key, + Body=manuscript_content.encode("utf-8"), + ContentType="text/markdown", + ) + + print(f" ✅ Uploaded to S3: s3://{bucket}/{key}") + + # Save to GitHub repo + if args.save_repo: + import requests + + print(f" 📤 Saving to GitHub: {args.save_repo}") + + github_token = os.environ.get("GITHUB_TOKEN") + if not github_token: + print(" ⚠️ GITHUB_TOKEN not set, cannot save to repo") + else: + # Parse owner/repo + owner, repo = args.save_repo.split("/") + + # Create filename + from datetime import datetime + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"manuscript_{timestamp}.md" + + # Get current branch + branch = args.save_branch + + # Create/update file via GitHub API + import base64 + + api_url = f"https://api.github.com/repos/{owner}/{repo}/contents/manuscripts/{filename}" + + headers = { + "Authorization": f"token {github_token}", + "Accept": "application/vnd.github.v3+json", + } + + # Check if file exists to get SHA + existing = requests.get(api_url, headers=headers) + sha = existing.json().get("sha") if existing.status_code == 200 else None + + # Create content + content_b64 = base64.b64encode(manuscript_content.encode("utf-8")).decode("utf-8") + + data = { + "message": args.save_commit_msg or f"Add generated manuscript: {filename}", + "content": content_b64, + "branch": branch, + } + if sha: + data["sha"] = sha + + resp = requests.put(api_url, headers=headers, json=data) + + if resp.status_code in [200, 201]: + print(f" ✅ Committed to GitHub: {args.save_repo}/manuscripts/{filename}") + else: + print(f" ⚠️ GitHub save failed: {resp.status_code} - {resp.text}") + print(f"\n{'='*60}") print(f"✅ COMPLETE!") print(f" Words: {word_count:,}") - print(f" Output: {output_path}") + if not args.output and not args.save_s3 and not args.save_repo: + print(f" Output: {output_path}") print(f"{'='*60}\n") return 0