Skip to content

Commit 1461cce

Browse files
authored
Merge pull request #34 from remyxai/terry/install-outrider-subcommand
Add `remyxai outrider init` subcommand (REMYX-72)
2 parents 283d346 + 7475f79 commit 1461cce

12 files changed

Lines changed: 1774 additions & 3 deletions

README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,68 @@ result, time_elapsed = run_inference(model_name, prompt, server_url="localhost:8
179179
print(result)
180180
```
181181

182+
### Outrider — weekly arXiv → draft PR for your repo
183+
184+
[Outrider](https://github.com/remyxai/outrider) is a GitHub Action that, on a weekly schedule, finds the most implementable recent paper for your repository and opens a draft PR wiring it into a real call site. `remyxai outrider` sets it up for you.
185+
186+
There are two ways to install it — both end with the same Action running in your repo; they differ in **who provisions it**:
187+
188+
| | `outrider init` | `outrider setup-local` |
189+
|---|---|---|
190+
| **Best for** | Most users | Teams that can't grant a third-party GitHub App yet (e.g. a pending security review) |
191+
| **How it works** | The **Remyx GitHub App** provisions it server-side; PRs are authored by `remyx-ai[bot]` | The CLI uses **your own `gh`** to provision it; PRs authored by `github-actions[bot]` |
192+
| **You provide** | The Remyx App installed on the repo (the command links you to it) | An authenticated `gh` with admin on the repo |
193+
194+
Either way you'll need a **Remyx API key** (from engine.remyx.ai → Settings) and an **Anthropic API key** (from console.anthropic.com). Set the Remyx key once:
195+
196+
```bash
197+
export REMYXAI_API_KEY=... # from engine.remyx.ai → Settings
198+
```
199+
200+
#### Option A — `outrider init` (via the Remyx GitHub App) — recommended
201+
202+
```bash
203+
# Set up on a repo, auto-creating a research interest from it:
204+
$ remyxai outrider init --repo owner/name --auto-interest
205+
206+
# …or use an existing research interest (UUID from engine.remyx.ai):
207+
$ remyxai outrider init --repo owner/name --interest <uuid>
208+
```
209+
210+
If the Remyx App isn't installed on the repo yet, the command prints an install link and waits. Then the engine provisions the workflow, repo secrets, and a setup PR, and — in the default `auto` mode — merges it and starts the first run. Connect your Anthropic key once on the engine's Integrations page, or pass `--anthropic-key` (or set `ANTHROPIC_API_KEY`) and the CLI connects it for you.
211+
212+
`--mode`: `auto` (default — set it up and start the first run), `review` (open the setup PR for you to merge), `off` (just create the research interest).
213+
214+
#### Option B — `outrider setup-local` (no GitHub App)
215+
216+
For teams that can't install a third-party App yet. The CLI uses your own authenticated `gh` to do everything — nothing new to security-review.
217+
218+
```bash
219+
$ export ANTHROPIC_API_KEY=... # stored as a repo secret by the CLI
220+
221+
$ remyxai outrider setup-local --repo owner/name --auto-interest
222+
```
223+
224+
Using your `gh` credentials, the CLI sets the `REMYX_API_KEY` + `ANTHROPIC_API_KEY` repo secrets, writes `.github/workflows/outrider.yml`, opens a setup PR, and — in `auto` mode — merges it and dispatches the first run. So the Action can open its recommendation PRs, the CLI enables the repo's *"Allow GitHub Actions to create and approve pull requests"* setting; the Action then uses the repo's built-in `GITHUB_TOKEN` (no GitHub token is stored as a secret — only `REMYX_API_KEY` and `ANTHROPIC_API_KEY`).
225+
226+
Requires `gh` authenticated (`gh auth login`, or `$GITHUB_TOKEN` with `repo` + `workflow` scopes) and admin on the target repo.
227+
228+
#### Common options
229+
230+
- `--repo owner/name` — target repo (or a GitHub URL); defaults to the current directory's git remote.
231+
- `--interest <uuid>` / `--auto-interest` — use an existing research interest, or create one from the repo.
232+
- `--mode auto|review` (`init` also has `off`) — how far to take setup.
233+
- `--dry-run` — print the plan (and, for `setup-local`, the rendered workflow) and exit without making changes.
234+
- `-y` / `--yes` — skip the confirmation prompt.
235+
236+
Preview either path safely before committing to it:
237+
238+
```bash
239+
$ remyxai outrider setup-local --repo owner/name --auto-interest --dry-run
240+
```
241+
242+
**A note on credentials:** with `setup-local`, your `REMYXAI_API_KEY` is stored as the repo's `REMYX_API_KEY` secret so the Action can fetch recommendations — anyone with write access to the repo's workflows can consume Remyx credits on that key, so use it on repos you control. With `outrider init`, the Remyx App provisions a scoped key for you. In both paths your Anthropic key is stored as a repo secret to run the agent.
243+
182244
### User
183245

184246
Get user profile info:

remyxai/api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def get_headers(api_key=None):
6767

6868
def log_api_response(response):
6969
"""Log the response from the API based on the status code."""
70-
if response.status_code in [200, 201]:
70+
if 200 <= response.status_code < 300:
7171
logging.debug(
7272
f"API call successful: {response.url}, Status: {response.status_code}"
7373
)

remyxai/api/github_app.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""
2+
remyxai/api/github_app.py
3+
4+
Client calls for the Remyx GitHub App (remyx-ai[bot]) endpoints.
5+
Wraps /api/v1.0/github/app/* in engine/app/api/github_app_api.py.
6+
7+
The App is what lets the engine provision the Outrider recommendation
8+
workflow, set repo secrets, and open bot-authored PRs without the user
9+
granting a personal token. Installing the App is an interactive browser
10+
step; these endpoints let the CLI surface the install link and poll for
11+
completion.
12+
"""
13+
from __future__ import annotations
14+
15+
import logging
16+
import requests
17+
from typing import Any, Dict, Optional
18+
19+
from . import BASE_URL, HEADERS, get_headers, log_api_response
20+
21+
logger = logging.getLogger(__name__)
22+
23+
24+
def _h(api_key: Optional[str] = None) -> dict:
25+
return get_headers(api_key) if api_key else HEADERS
26+
27+
28+
def get_app_install_url(api_key: Optional[str] = None) -> Dict[str, Any]:
29+
"""Return the Remyx GitHub App install URL for the current user.
30+
31+
Calls GET /api/v1.0/github/app/install-url
32+
33+
Returns { install_url, state, app_slug, configured }. When the App
34+
isn't configured on the server, returns { configured: False, error }.
35+
"""
36+
r = requests.get(
37+
f"{BASE_URL}/github/app/install-url", headers=_h(api_key), timeout=30
38+
)
39+
log_api_response(r)
40+
if r.status_code == 503:
41+
return r.json() # {configured: False, error}
42+
r.raise_for_status()
43+
return r.json()
44+
45+
46+
def is_app_installed(repo: str, api_key: Optional[str] = None) -> bool:
47+
"""Whether the Remyx App is installed on `repo` (owner/name).
48+
49+
Calls GET /api/v1.0/github/app/installation?repo=owner/name
50+
"""
51+
r = requests.get(
52+
f"{BASE_URL}/github/app/installation",
53+
params={"repo": repo},
54+
headers=_h(api_key),
55+
timeout=30,
56+
)
57+
log_api_response(r)
58+
r.raise_for_status()
59+
return bool(r.json().get("installed"))

remyxai/api/integrations.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""
2+
remyxai/api/integrations.py
3+
4+
Client calls for the JWT integrations API.
5+
Wraps /api/v1.0/integrations/* in engine/app/api/integrations.py.
6+
7+
Used by the CLI to connect/check credential-based providers — notably
8+
the model provider (`claude_code` → Anthropic key) that Outrider needs
9+
the engine to push to the repo as a secret.
10+
"""
11+
from __future__ import annotations
12+
13+
import logging
14+
import requests
15+
from typing import Any, Dict, Optional
16+
17+
from . import BASE_URL, HEADERS, get_headers, log_api_response
18+
19+
logger = logging.getLogger(__name__)
20+
21+
22+
def _h(api_key: Optional[str] = None) -> dict:
23+
return get_headers(api_key) if api_key else HEADERS
24+
25+
26+
def get_integration_status(
27+
provider: str, api_key: Optional[str] = None
28+
) -> Dict[str, Any]:
29+
"""Connection status for a provider.
30+
31+
Calls GET /api/v1.0/integrations/<provider>/status
32+
Returns { connected: bool, connection?: {...} }.
33+
"""
34+
r = requests.get(
35+
f"{BASE_URL}/integrations/{provider}/status",
36+
headers=_h(api_key),
37+
timeout=30,
38+
)
39+
log_api_response(r)
40+
r.raise_for_status()
41+
return r.json()
42+
43+
44+
def connect_credential(
45+
provider: str,
46+
credentials: Dict[str, Any],
47+
api_key: Optional[str] = None,
48+
) -> Dict[str, Any]:
49+
"""Connect a credential-based provider (e.g. claude_code).
50+
51+
Calls POST /api/v1.0/integrations/<provider>/connect
52+
Body is the provider's credential dict, e.g. {"api_key": "sk-ant-..."}.
53+
The engine validates the key against the provider before storing it
54+
encrypted. Returns { connection, user_info } on success.
55+
"""
56+
r = requests.post(
57+
f"{BASE_URL}/integrations/{provider}/connect",
58+
json=credentials,
59+
headers=_h(api_key),
60+
timeout=30,
61+
)
62+
log_api_response(r)
63+
r.raise_for_status()
64+
return r.json()

remyxai/api/interests.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,93 @@ def toggle_interest(
164164
log_api_response(r)
165165
r.raise_for_status()
166166
return r.json()
167+
168+
169+
# ─── Outrider provisioning (REMYX-60) ──────────────────────────────────────
170+
171+
def provision_action(
172+
interest_id: str,
173+
repo_url: Optional[str] = None,
174+
auto_merge: bool = True,
175+
branch: Optional[str] = None,
176+
workflow_filename: Optional[str] = None,
177+
api_key: Optional[str] = None,
178+
) -> Dict[str, Any]:
179+
"""
180+
Provision the Outrider recommendation Action on a repo, server-side
181+
(the Remyx GitHub App sets secrets, writes the workflow, opens a
182+
bot-authored setup PR, and — when auto_merge — merges it and fires
183+
the first run).
184+
185+
Calls POST /api/v1.0/interests/<id>/provision-action
186+
187+
Args:
188+
repo_url: GitHub URL; defaults to the interest's source repo.
189+
auto_merge: merge the setup PR + dispatch the first run ("set it
190+
up for me"). False opens the PR for the user to review.
191+
192+
Returns 202 { task_id, status_url }. Poll with poll_provision_action.
193+
"""
194+
payload: Dict[str, Any] = {"auto_merge": auto_merge}
195+
if repo_url:
196+
payload["repo_url"] = repo_url
197+
if branch:
198+
payload["branch"] = branch
199+
if workflow_filename:
200+
payload["workflow_filename"] = workflow_filename
201+
202+
r = requests.post(
203+
f"{BASE_URL}/interests/{interest_id}/provision-action",
204+
json=payload,
205+
headers=_h(api_key),
206+
timeout=30,
207+
)
208+
log_api_response(r)
209+
r.raise_for_status()
210+
return r.json()
211+
212+
213+
def poll_provision_action(
214+
interest_id: str,
215+
task_id: str,
216+
api_key: Optional[str] = None,
217+
) -> Dict[str, Any]:
218+
"""
219+
Poll a provision-action task.
220+
221+
Calls GET /api/v1.0/interests/<id>/provision-action/<task_id>
222+
223+
Returns the task dict: { status, progress, message, result, error }.
224+
On completion, `result` carries pr_url, secret_set, merged, dispatched,
225+
model_key_missing, key_label, repo.
226+
"""
227+
r = requests.get(
228+
f"{BASE_URL}/interests/{interest_id}/provision-action/{task_id}",
229+
headers=_h(api_key),
230+
timeout=30,
231+
)
232+
log_api_response(r)
233+
r.raise_for_status()
234+
return r.json()
235+
236+
237+
def get_provision_status(
238+
interest_id: str,
239+
api_key: Optional[str] = None,
240+
) -> Dict[str, Any]:
241+
"""
242+
Latest provisioning state for an interest.
243+
244+
Calls GET /api/v1.0/interests/<id>/provision-action
245+
246+
Returns { provisioned: False } if none, else the provisioned-action
247+
record with provisioned: True.
248+
"""
249+
r = requests.get(
250+
f"{BASE_URL}/interests/{interest_id}/provision-action",
251+
headers=_h(api_key),
252+
timeout=30,
253+
)
254+
log_api_response(r)
255+
r.raise_for_status()
256+
return r.json()

0 commit comments

Comments
 (0)