Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,13 @@ No user intervention required - complete end-to-end. All configuration and deplo

## Usage

The CLI has five main commands:
The CLI has six main commands:

- `devin-cli auth` - Manage authentication
- `devin-cli create` - Create Devin sessions
- `devin-cli get` - Get details of existing Devin sessions
- `devin-cli message` - Send messages to existing Devin sessions
- `devin-cli upload` - Upload files for use in Devin sessions
- `devin-cli setup` - Download workflow templates and guides

### Creating Sessions
Expand Down Expand Up @@ -244,6 +245,42 @@ The `message` command:
- Confirms successful message delivery
- Returns message ID and session status

### Uploading Files

Use the `upload` command to upload files to Devin for use in session prompts:

```bash
# Upload a file (displays table format with usage instructions)
devin-cli upload /path/to/file.py

# Upload and get JSON response
devin-cli upload /path/to/file.csv --output json

# Upload and display ready-to-use format
devin-cli upload /path/to/data.json --format-prompt
```

The `upload` command:
- Accepts a file path as a required argument
- Returns the attachment URL from the API
- Displays instructions on how to use the file in prompts
- Supports various file types (code, text, CSV, JSON, images, etc.)

**Using uploaded files in session prompts:**

After uploading a file, include it in your session prompt using the ATTACHMENT format:

```bash
# Create a session with an attached file
devin-cli create --prompt "Review this code file

ATTACHMENT:\"https://storage.devin.ai/attachments/abc123/file.py\"

Please analyze it for potential bugs."
```

**Important:** The ATTACHMENT line must be on its own line in the prompt for Devin to recognize it.

### Setting Up Templates

The `setup` command downloads the latest workflow templates and session guide to your repository:
Expand Down Expand Up @@ -306,6 +343,11 @@ For the `devin-cli message` command:
- `--message, -m`: The message to send (will be prompted if not provided)
- `--output, -o`: Output format (`json` or `table`, default: `table`)

For the `devin-cli upload` command:
- `FILE_PATH`: Path to the file to upload (required)
- `--output, -o`: Output format (`json` or `table`, default: `table`)
- `--format-prompt`: Display the ready-to-use ATTACHMENT: format

### Global Options

- `--version`: Show version information
Expand Down Expand Up @@ -355,6 +397,19 @@ devin-cli get devin-session-123 --output json # Get details as JSON
devin-cli message devin-session-123 # Send message interactively
devin-cli message devin-session-123 --message "Please continue with the implementation"

# Upload examples
devin-cli upload requirements.txt # Upload a file
devin-cli upload data.csv --output json # Get JSON output
devin-cli upload config.json --format-prompt # Show ready-to-use format

# Combined workflow: upload file and create session
URL=$(devin-cli upload analysis.py --output json | jq -r '.url')
devin-cli create --prompt "Review this code

ATTACHMENT:\"$URL\"

Check for security issues."

# Setup examples
devin-cli setup # Download templates to current directory
devin-cli setup --force # Overwrite existing files
Expand Down
65 changes: 65 additions & 0 deletions devin_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,36 @@ def send_message_to_session(session_id: str, payload: dict) -> dict:
raise DevinAPIError(f"API request failed: {e}")


def upload_file(file_path: str) -> str:
"""Upload a file to Devin attachments API and return the URL"""
api_key = get_api_key()

if not os.path.exists(file_path):
raise DevinAPIError(f"File not found: {file_path}")

headers = {
'Authorization': f'Bearer {api_key}'
}

try:
with open(file_path, 'rb') as f:
files = {'file': f}
response = requests.post(
'https://api.devin.ai/v1/attachments',
headers=headers,
files=files,
timeout=60
)
response.raise_for_status()

url = response.text.strip()
if url.startswith('"') and url.endswith('"'):
url = url[1:-1]
return url
except requests.exceptions.RequestException as e:
raise DevinAPIError(f"File upload failed: {e}")


def parse_list_input(value: str) -> List[str]:
"""Parse comma-separated string into list"""
if not value:
Expand Down Expand Up @@ -379,6 +409,41 @@ def message(session_id, message, output):
click.echo(f"❌ Unexpected error: {e}", err=True)
sys.exit(1)

@cli.command()
@click.argument('file_path', type=click.Path(exists=True))
@click.option('--output', '-o', type=click.Choice(['json', 'table']), default='table', help='Output format')
@click.option('--format-prompt', is_flag=True, help='Output the ready-to-use ATTACHMENT: format')
def upload(file_path, output, format_prompt):
"""Upload a file to Devin for use in session prompts"""

try:
click.echo(f"Uploading file: {file_path}...")
url = upload_file(file_path)

if output == 'json':
result = {'url': url}
if format_prompt:
result['formatted'] = f'ATTACHMENT:"{url}"'
click.echo(json.dumps(result, indent=2))
else:
click.echo("\n✅ File uploaded successfully!")
click.echo(f"URL: {url}")
click.echo("\n📋 To use this file in a Devin session prompt, include:")
click.echo(f'ATTACHMENT:"{url}"')

if format_prompt:
click.echo("\n📝 Ready-to-use format:")
click.echo(f'ATTACHMENT:"{url}"')

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)')
Expand Down
116 changes: 116 additions & 0 deletions test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,116 @@ def test_get_and_message_api_error_handling():
print("✅ API error handling for new commands works correctly")


def test_upload_help():
"""Test upload subcommand help"""
print("🧪 Testing upload --help command...")
result = run_cli_command(['upload', '--help'])

assert result['returncode'] == 0, f"Upload help command failed: {result['stderr']}"
assert 'Upload a file to Devin' in result['stdout'], "Upload help text missing"
assert '--output' in result['stdout'], "Output option missing from upload help"
assert '--format-prompt' in result['stdout'], "Format-prompt option missing from upload help"
print("✅ Upload help command works correctly")


def test_upload_file_function():
"""Test upload_file function with mocked API"""
print("🧪 Testing upload_file function...")

sys.path.insert(0, os.path.dirname(__file__))
from devin_cli import upload_file

with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
f.write("Test content")
test_file = f.name

try:
with patch('devin_cli.requests.post') as mock_post:
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.text = '"https://storage.devin.ai/attachments/test-id/file.txt"'
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response

with patch('devin_cli.get_api_key', return_value='test-key'):
url = upload_file(test_file)

assert mock_post.call_count == 1, "API should have been called once"
call_kwargs = mock_post.call_args[1]
assert call_kwargs['headers']['Authorization'] == 'Bearer test-key'
assert 'files' in call_kwargs
assert call_kwargs['timeout'] == 60

assert url == 'https://storage.devin.ai/attachments/test-id/file.txt'
assert not url.startswith('"'), "URL should not have quotes"

print("✅ Upload file function works correctly")
finally:
os.unlink(test_file)


def test_upload_file_not_found():
"""Test upload_file with non-existent file"""
print("🧪 Testing upload with non-existent file...")

sys.path.insert(0, os.path.dirname(__file__))
from devin_cli import upload_file, DevinAPIError

with patch('devin_cli.get_api_key', return_value='test-key'):
try:
upload_file('/nonexistent/file.txt')
assert False, "Should have raised DevinAPIError"
except DevinAPIError as e:
assert 'File not found' in str(e)

print("✅ File not found error handling works correctly")


def test_upload_api_error():
"""Test upload_file with API error"""
print("🧪 Testing upload with API error...")

sys.path.insert(0, os.path.dirname(__file__))
from devin_cli import upload_file, DevinAPIError

with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
f.write("Test content")
test_file = f.name

try:
with patch('devin_cli.requests.post') as mock_post:
mock_post.side_effect = requests.exceptions.RequestException("API Error")

with patch('devin_cli.get_api_key', return_value='test-key'):
try:
upload_file(test_file)
assert False, "Should have raised DevinAPIError"
except DevinAPIError as e:
assert 'File upload failed' in str(e)

print("✅ API error handling works correctly")
finally:
os.unlink(test_file)


def test_upload_command_cli_execution():
"""Test upload command via CLI"""
print("🧪 Testing upload command CLI execution...")

with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
f.write("Test content for CLI")
test_file = f.name

try:
result = run_cli_command(['upload', '--help'])
assert result['returncode'] == 0, "Upload help should work"
assert 'Upload a file' in result['stdout']

print("✅ Upload command CLI execution works correctly")
finally:
os.unlink(test_file)


def run_all_tests():
"""Run all tests"""
print("🚀 Starting comprehensive CLI tests...\n")
Expand Down Expand Up @@ -1087,6 +1197,12 @@ def run_all_tests():
# Integration
test_api_request_structure,
test_end_to_end_workflow,

test_upload_help,
test_upload_file_function,
test_upload_file_not_found,
test_upload_api_error,
test_upload_command_cli_execution,
]

passed = 0
Expand Down