From 0f672b3d4e8a2bce3f8375661a6c90f2d2be1521 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:05:13 +0000 Subject: [PATCH] Add upload command for sending images/files to Devin sessions - Add upload_file_to_attachments() API function that uploads files to /v1/attachments endpoint - Add upload CLI command that takes session_id and --image/-i option - Command uploads file and sends message to session with attachment URL - Follows existing patterns from message command and send_message_to_session() - Supports both json and table output formats - Uses multipart/form-data encoding via requests files parameter Co-Authored-By: parker.duff@codeium.com --- devin_cli.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/devin_cli.py b/devin_cli.py index 6d12cac..31391a5 100755 --- a/devin_cli.py +++ b/devin_cli.py @@ -166,6 +166,30 @@ def send_message_to_session(session_id: str, payload: dict) -> dict: raise DevinAPIError(f"API request failed: {e}") +def upload_file_to_attachments(file_path: str) -> str: + """Upload a file to Devin attachments and return the URL""" + api_key = get_api_key() + + headers = { + 'Authorization': f'Bearer {api_key}' + } + + try: + with open(file_path, 'rb') as f: + response = requests.post( + 'https://api.devin.ai/v1/attachments', + headers=headers, + files={'file': f}, + timeout=30 + ) + response.raise_for_status() + return response.text.strip() + except requests.exceptions.RequestException as e: + raise DevinAPIError(f"API request failed: {e}") + except FileNotFoundError: + raise DevinAPIError(f"File not found: {file_path}") + + def parse_list_input(value: str) -> List[str]: """Parse comma-separated string into list""" if not value: @@ -380,6 +404,54 @@ def message(session_id, message, output): sys.exit(1) +@cli.command() +@click.argument('session_id') +@click.option('--image', '-i', 'file_path', help='Path to image or file to upload') +@click.option('--message', '-m', help='Optional message to send with the attachment') +@click.option('--output', '-o', type=click.Choice(['json', 'table']), default='table', help='Output format') +def upload(session_id, file_path, message, output): + """Upload an image or file to an existing Devin session""" + + if not file_path: + file_path = click.prompt('Path to file to upload', type=str) + + if not os.path.exists(file_path): + click.echo(f"❌ Error: File not found: {file_path}", err=True) + sys.exit(1) + + try: + click.echo(f"Uploading file {file_path}...") + file_url = upload_file_to_attachments(file_path) + click.echo(f"✅ File uploaded successfully") + + if message: + message_text = f"{message}\n\nATTACHMENT:\"{file_url}\"" + else: + message_text = f"ATTACHMENT:\"{file_url}\"" + + click.echo(f"Sending attachment to session {session_id}...") + payload = {'message': message_text} + result = send_message_to_session(session_id, payload) + + if output == 'json': + result['file_url'] = file_url + click.echo(json.dumps(result, indent=2)) + else: + click.echo("\n✅ Attachment sent successfully!") + click.echo(f"File URL: {file_url}") + if 'message_id' in result: + click.echo(f"Message ID: {result['message_id']}") + if 'status' in result: + click.echo(f"Session Status: {result['status']}") + + except DevinAPIError as e: + click.echo(f"❌ Error: {e}", err=True) + sys.exit(1) + except Exception as e: + click.echo(f"❌ Unexpected error: {e}", err=True) + sys.exit(1) + + @cli.command() @click.option('--target-dir', '-t', default='.', help='Target directory to copy files to (default: current directory)') @click.option('--force', '-f', is_flag=True, help='Overwrite existing files without prompting')