Skip to content
Merged
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
40 changes: 40 additions & 0 deletions docs/connectors_list.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,43 @@ CLI flow:
3. Run `octopal connector auth google`
4. Run `octopal connector status`
5. Restart Octopal if needed

## GitHub

Current supported services:
- Repositories
- Issues
- Pull requests

What it can do today:
- inspect the authenticated GitHub account
- list repositories visible to that account
- inspect repository metadata
- list repository issues
- read a single issue
- create issues
- update issues
- list issue and pull request conversation comments
- create issue and pull request conversation comments
- update issue and pull request conversation comments
- list pull requests
- read a single pull request
- list pull request reviews
- list inline review comments on pull requests
- create pull request reviews including comment/approve/request changes
- list changed files in pull requests, including patch hunks when GitHub provides them
- list commits included in pull requests
- list commit comments for specific commit SHAs
- summarize pull request merge readiness without merging

What it does not do yet:
- create or merge pull requests
- read GitHub Actions runs
- read code contents through the connector

CLI flow:
1. Run `octopal configure`
2. Enable any needed GitHub services such as `Repositories`, `Issues`, and/or `Pull Requests`
3. Run `octopal connector auth github`
4. Run `octopal connector status`
5. Restart Octopal if needed
51 changes: 50 additions & 1 deletion src/octopal/cli/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,8 @@ def _configure_connectors(config: OctopalConfig, prompter) -> None:
"Connectors (experimental)",
[
"Connectors allow Octo to link with external services through explicit CLI setup.",
"Google connector support currently covers Gmail and Calendar.",
"Google connector support covers Gmail, Calendar, and Drive.",
"GitHub connector support covers repositories, issues, and pull requests.",
"After saving, Octopal will tell you which CLI command to run next for authorization.",
],
)
Expand All @@ -600,6 +601,11 @@ def _configure_connectors(config: OctopalConfig, prompter) -> None:
label="Google",
hint="Integrate with Gmail and Google Calendar. More Google services can land on the same connector flow later.",
),
WizardSelectOption(
value="github",
label="GitHub",
hint="Inspect repositories, issues, and pull requests with a personal access token.",
),
]

initial_values = [
Expand Down Expand Up @@ -649,6 +655,26 @@ def _configure_connectors(config: OctopalConfig, prompter) -> None:
)
)
config.connectors.instances[name].enabled_services = selected_google
if name == "github" and is_enabled:
github_services = [
WizardSelectOption(value="repos", label="Repositories"),
WizardSelectOption(value="issues", label="Issues"),
WizardSelectOption(value="pull_requests", label="Pull Requests"),
]

current_github_services = config.connectors.instances[name].enabled_services or ["repos"]
current_github_services = [
service for service in current_github_services if service in {"repos", "issues", "pull_requests"}
] or ["repos"]

selected_github = prompter.multiselect(
WizardMultiSelectParams(
message="Select specific GitHub services to enable",
initial_values=current_github_services,
options=github_services,
)
)
config.connectors.instances[name].enabled_services = selected_github


def _build_sections(config: OctopalConfig, prompter) -> list[WizardSection]:
Expand Down Expand Up @@ -850,6 +876,29 @@ def _collect_connector_next_steps(config: OctopalConfig, previous_config: Octopa
" [magenta]octopal restart[/magenta] - Reload Octopal after connector authorization."
)

github = config.connectors.instances.get("github")
if github and github.enabled:
current_services = set(_enabled_services(config, "github"))
previous_services = set(_enabled_services(previous_config, "github"))
authorized_services = set(_authorized_services(config, "github"))
has_token = bool(github.auth.access_token)
previous_github = previous_config.connectors.instances.get("github")

needs_auth = not has_token or not current_services.issubset(authorized_services)
services_added = sorted(current_services - previous_services)
newly_enabled = previous_github is None or not previous_github.enabled

if needs_auth or services_added or newly_enabled:
lines.append(
" [magenta]octopal connector auth github[/magenta] - Authorize GitHub for the enabled services."
)
lines.append(
" [magenta]octopal connector status[/magenta] - Verify connector status after authorization."
)
lines.append(
" [magenta]octopal restart[/magenta] - Reload Octopal after connector authorization."
)

return lines


Expand Down
53 changes: 36 additions & 17 deletions src/octopal/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1087,11 +1087,11 @@ def _connector_disconnect_message(name: str, *, forget_credentials: bool) -> str
if forget_credentials:
return (
f"[bold yellow]{name} disconnected.[/bold yellow] "
"Stored client credentials were removed."
"Stored credentials were removed."
)
return (
f"[bold yellow]{name} disconnected.[/bold yellow] "
"Stored client credentials were kept so you can re-authorize later."
"Stored credentials were kept so you can re-authorize later."
)


Expand Down Expand Up @@ -1122,6 +1122,18 @@ def _print_google_headless_auth_help(auth_url: str) -> None:
console.print()


def _print_github_auth_setup_help() -> None:
console.print()
console.print("[bold]GitHub token setup[/bold]")
console.print("You need a GitHub personal access token for the account Octopal should use.")
console.print("Fine-grained tokens are preferred when they cover the repositories you want.")
console.print(" 1. Open [cyan]https://github.com/settings/personal-access-tokens[/cyan]")
console.print(" 2. Create a fine-grained token or a classic token if your setup needs it")
console.print(" 3. Grant read access to repository metadata, issues, and pull requests as needed")
console.print(" 4. Paste the token here when prompted")
console.print()


@connector_app.command("status")
def connector_status(json_output: bool = typer.Option(False, "--json", help="Print machine-readable JSON.")) -> None:
"""Show connector status and any required next action."""
Expand Down Expand Up @@ -1160,6 +1172,7 @@ def connector_auth(
name: str = typer.Argument(..., help="Connector name to authorize."),
client_id: str | None = typer.Option(None, "--client-id", help="OAuth client ID to use."),
client_secret: str | None = typer.Option(None, "--client-secret", help="OAuth client secret to use."),
token: str | None = typer.Option(None, "--token", help="Access token to use for token-based connectors."),
) -> None:
"""Authorize a configured connector via CLI."""
settings = load_settings()
Expand All @@ -1178,22 +1191,28 @@ def connector_auth(

if name == "google":
_print_google_auth_setup_help()
resolved_client_id = client_id or typer.prompt(
"Your Google OAuth Desktop App client ID"
)
resolved_client_secret = client_secret or typer.prompt(
"Your Google OAuth Desktop App client secret",
hide_input=True,
)

asyncio.run(
connector.configure(
{
"client_id": resolved_client_id,
"client_secret": resolved_client_secret,
}
resolved_client_id = client_id or typer.prompt(
"Your Google OAuth Desktop App client ID"
)
)
resolved_client_secret = client_secret or typer.prompt(
"Your Google OAuth Desktop App client secret",
hide_input=True,
)
asyncio.run(
connector.configure(
{
"client_id": resolved_client_id,
"client_secret": resolved_client_secret,
}
)
)
elif name == "github":
_print_github_auth_setup_help()
resolved_token = token or typer.prompt(
"Your GitHub personal access token",
hide_input=True,
)
asyncio.run(connector.configure({"token": resolved_token}))

result = asyncio.run(connector.authorize())
if result.get("status") == "manual_required" and name == "google":
Expand Down
Loading