Skip to content

Commit 54385b7

Browse files
authored
Rpatel/cli updates (#1204)
# Motivation <!-- Why is this change necessary? --> # Content <!-- Please include a summary of the change --> # Testing <!-- How was the change tested? --> # Please check the following before marking your PR as ready for review - [ ] I have added tests for my changes - [x] I have updated the documentation or added new documentation as needed
1 parent 94be7bf commit 54385b7

File tree

15 files changed

+2175
-113
lines changed

15 files changed

+2175
-113
lines changed

src/codegen/cli/auth/token_manager.py

Lines changed: 172 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,15 @@ def save_token_with_org_info(self, token: str) -> None:
5858
# Add organization info if available
5959
orgs = org_data.get("items", [])
6060
if orgs and len(orgs) > 0:
61-
primary_org = orgs[0] # Use first org as primary
62-
auth_data["organization"] = {"id": primary_org.get("id"), "name": primary_org.get("name"), "all_orgs": [{"id": org.get("id"), "name": org.get("name")} for org in orgs]}
61+
# Store ALL organizations in cache for local resolution
62+
all_orgs = [{"id": org.get("id"), "name": org.get("name")} for org in orgs]
63+
primary_org = orgs[0] # Use first org as primary/default
64+
auth_data["organization"] = {
65+
"id": primary_org.get("id"),
66+
"name": primary_org.get("name"),
67+
"all_orgs": all_orgs
68+
}
69+
auth_data["organizations_cache"] = all_orgs # Separate cache for easy access
6370

6471
except requests.RequestException as e:
6572
# If we can't fetch org info, still save the token but without org data
@@ -171,6 +178,53 @@ def get_user_info(self) -> dict | None:
171178
return auth_data["user"]
172179
return None
173180

181+
def get_cached_organizations(self) -> list[dict] | None:
182+
"""Get all cached organizations.
183+
184+
Returns:
185+
List of organization dictionaries with 'id' and 'name' keys, or None if no cache.
186+
"""
187+
auth_data = self.get_auth_data()
188+
if auth_data and "organizations_cache" in auth_data:
189+
return auth_data["organizations_cache"]
190+
# Fallback to legacy format
191+
if auth_data and "organization" in auth_data and "all_orgs" in auth_data["organization"]:
192+
return auth_data["organization"]["all_orgs"]
193+
return None
194+
195+
def is_org_id_in_cache(self, org_id: int) -> bool:
196+
"""Check if an organization ID exists in the local cache.
197+
198+
Args:
199+
org_id: The organization ID to check
200+
201+
Returns:
202+
True if the organization ID is found in cache, False otherwise.
203+
"""
204+
cached_orgs = self.get_cached_organizations()
205+
if not cached_orgs:
206+
return False
207+
208+
return any(org.get("id") == org_id for org in cached_orgs)
209+
210+
def get_org_name_from_cache(self, org_id: int) -> str | None:
211+
"""Get organization name from cache by ID.
212+
213+
Args:
214+
org_id: The organization ID to look up
215+
216+
Returns:
217+
Organization name if found in cache, None otherwise.
218+
"""
219+
cached_orgs = self.get_cached_organizations()
220+
if not cached_orgs:
221+
return None
222+
223+
for org in cached_orgs:
224+
if org.get("id") == org_id:
225+
return org.get("name")
226+
return None
227+
174228

175229
def get_current_token() -> str | None:
176230
"""Get the current authentication token if one exists.
@@ -233,6 +287,42 @@ def get_current_org_name() -> str | None:
233287
return token_manager.get_org_name()
234288

235289

290+
def get_cached_organizations() -> list[dict] | None:
291+
"""Get all cached organizations.
292+
293+
Returns:
294+
List of organization dictionaries with 'id' and 'name' keys, or None if no cache.
295+
"""
296+
token_manager = TokenManager()
297+
return token_manager.get_cached_organizations()
298+
299+
300+
def is_org_id_cached(org_id: int) -> bool:
301+
"""Check if an organization ID exists in the local cache.
302+
303+
Args:
304+
org_id: The organization ID to check
305+
306+
Returns:
307+
True if the organization ID is found in cache, False otherwise.
308+
"""
309+
token_manager = TokenManager()
310+
return token_manager.is_org_id_in_cache(org_id)
311+
312+
313+
def get_org_name_from_cache(org_id: int) -> str | None:
314+
"""Get organization name from cache by ID.
315+
316+
Args:
317+
org_id: The organization ID to look up
318+
319+
Returns:
320+
Organization name if found in cache, None otherwise.
321+
"""
322+
token_manager = TokenManager()
323+
return token_manager.get_org_name_from_cache(org_id)
324+
325+
236326
def get_current_user_info() -> dict | None:
237327
"""Get the stored user info if available.
238328
@@ -241,3 +331,83 @@ def get_current_user_info() -> dict | None:
241331
"""
242332
token_manager = TokenManager()
243333
return token_manager.get_user_info()
334+
335+
336+
# Repository caching functions (similar to organization caching)
337+
338+
def get_cached_repositories() -> list[dict] | None:
339+
"""Get all cached repositories.
340+
341+
Returns:
342+
List of repository dictionaries with 'id' and 'name' keys, or None if no cache.
343+
"""
344+
token_manager = TokenManager()
345+
auth_data = token_manager.get_auth_data()
346+
if auth_data and "repositories_cache" in auth_data:
347+
return auth_data["repositories_cache"]
348+
return None
349+
350+
351+
def cache_repositories(repositories: list[dict]) -> None:
352+
"""Cache repositories to local storage.
353+
354+
Args:
355+
repositories: List of repository dictionaries to cache
356+
"""
357+
token_manager = TokenManager()
358+
auth_data = token_manager.get_auth_data()
359+
if auth_data:
360+
auth_data["repositories_cache"] = repositories
361+
# Save back to file
362+
try:
363+
import json
364+
with open(token_manager.token_file, 'w') as f:
365+
json.dump(auth_data, f, indent=2)
366+
except Exception:
367+
pass # Fail silently
368+
369+
370+
def is_repo_id_cached(repo_id: int) -> bool:
371+
"""Check if a repository ID exists in the local cache.
372+
373+
Args:
374+
repo_id: The repository ID to check
375+
376+
Returns:
377+
True if the repository ID is found in cache, False otherwise.
378+
"""
379+
cached_repos = get_cached_repositories()
380+
if not cached_repos:
381+
return False
382+
383+
return any(repo.get("id") == repo_id for repo in cached_repos)
384+
385+
386+
def get_repo_name_from_cache(repo_id: int) -> str | None:
387+
"""Get repository name from cache by ID.
388+
389+
Args:
390+
repo_id: The repository ID to look up
391+
392+
Returns:
393+
Repository name if found in cache, None otherwise.
394+
"""
395+
cached_repos = get_cached_repositories()
396+
if not cached_repos:
397+
return None
398+
399+
for repo in cached_repos:
400+
if repo.get("id") == repo_id:
401+
return repo.get("name")
402+
403+
return None
404+
405+
406+
def get_current_repo_name() -> str | None:
407+
"""Get the current repository name from environment or cache."""
408+
from codegen.cli.utils.repo import get_current_repo_id
409+
410+
repo_id = get_current_repo_id()
411+
if repo_id:
412+
return get_repo_name_from_cache(repo_id)
413+
return None

src/codegen/cli/cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
from codegen.cli.commands.integrations.main import integrations_app
1515
from codegen.cli.commands.login.main import login
1616
from codegen.cli.commands.logout.main import logout
17+
from codegen.cli.commands.org.main import org
1718
from codegen.cli.commands.profile.main import profile
19+
from codegen.cli.commands.repo.main import repo
1820
from codegen.cli.commands.style_debug.main import style_debug
1921
from codegen.cli.commands.tools.main import tools
2022
from codegen.cli.commands.tui.main import tui
@@ -39,7 +41,9 @@ def version_callback(value: bool):
3941
main.command("init", help="Initialize or update the Codegen folder.")(init)
4042
main.command("login", help="Store authentication token.")(login)
4143
main.command("logout", help="Clear stored authentication token.")(logout)
44+
main.command("org", help="Manage and switch between organizations.")(org)
4245
main.command("profile", help="Display information about the currently authenticated user.")(profile)
46+
main.command("repo", help="Manage repository configuration and environment variables.")(repo)
4347
main.command("style-debug", help="Debug command to visualize CLI styling (spinners, etc).")(style_debug)
4448
main.command("tools", help="List available tools from the Codegen API.")(tools)
4549
main.command("tui", help="Launch the interactive TUI interface.")(tui)

src/codegen/cli/commands/claude/main.py

Lines changed: 92 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,82 @@
77
import threading
88
import time
99

10+
import requests
1011
import typer
12+
from rich import box
13+
from rich.panel import Panel
14+
15+
16+
from codegen.cli.api.endpoints import API_ENDPOINT
17+
from codegen.cli.auth.token_manager import get_current_token
1118
from codegen.cli.commands.claude.claude_log_watcher import ClaudeLogWatcherManager
1219
from codegen.cli.commands.claude.claude_session_api import end_claude_session, generate_session_id
1320
from codegen.cli.commands.claude.config.mcp_setup import add_codegen_mcp_server, cleanup_codegen_mcp_server
1421
from codegen.cli.commands.claude.hooks import cleanup_claude_hook, ensure_claude_hook, get_codegen_url
1522
from codegen.cli.commands.claude.quiet_console import console
23+
from rich.console import Console
24+
25+
t_console = Console()
26+
27+
from codegen.cli.rich.spinners import create_spinner
1628
from codegen.cli.utils.org import resolve_org_id
1729

1830

19-
def claude(
20-
org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)"),
21-
no_mcp: bool | None = typer.Option(False, "--no-mcp", help="Disable Codegen's MCP server with additional capabilities over HTTP"),
22-
):
23-
"""Run Claude Code with session tracking.
31+
def _run_claude_background(resolved_org_id: int, prompt: str | None) -> None:
32+
"""Create a background agent run with Claude context and exit."""
33+
token = get_current_token()
34+
if not token:
35+
console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.")
36+
raise typer.Exit(1)
37+
38+
payload = {"prompt": prompt or "Start a Claude Code background session"}
2439

25-
This command runs Claude Code and tracks the session in the backend API:
26-
- Generates a unique session ID
27-
- Creates an agent run when Claude starts
28-
- Updates the agent run status when Claude exits
29-
"""
40+
spinner = create_spinner("Creating agent run...")
41+
spinner.start()
42+
try:
43+
headers = {
44+
"Authorization": f"Bearer {token}",
45+
"Content-Type": "application/json",
46+
"x-codegen-client": "codegen__claude_code",
47+
}
48+
url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/agent/run"
49+
response = requests.post(url, headers=headers, json=payload)
50+
response.raise_for_status()
51+
agent_run_data = response.json()
52+
finally:
53+
spinner.stop()
54+
55+
run_id = agent_run_data.get("id", "Unknown")
56+
status = agent_run_data.get("status", "Unknown")
57+
web_url = agent_run_data.get("web_url", "")
58+
59+
result_lines = [
60+
f"[cyan]Agent Run ID:[/cyan] {run_id}",
61+
f"[cyan]Status:[/cyan] {status}",
62+
]
63+
if web_url:
64+
result_lines.append(f"[cyan]Web URL:[/cyan] {web_url}")
65+
66+
t_console.print(
67+
Panel(
68+
"\n".join(result_lines),
69+
title="🤖 [bold]Background Agent Run Created[/bold]",
70+
border_style="green",
71+
box=box.ROUNDED,
72+
padding=(1, 2),
73+
)
74+
)
75+
t_console.print("\n[dim]💡 Track progress with:[/dim] [cyan]codegen agents[/cyan]")
76+
if web_url:
77+
t_console.print(f"[dim]🌐 View in browser:[/dim] [link]{web_url}[/link]")
78+
79+
80+
def _run_claude_interactive(resolved_org_id: int, no_mcp: bool | None) -> None:
81+
"""Launch Claude Code with session tracking and log watching."""
3082
# Generate session ID for tracking
3183
session_id = generate_session_id()
3284
console.print(f"🆔 Generated session ID: {session_id[:8]}...", style="dim")
33-
34-
# Resolve org_id early for session management
35-
resolved_org_id = resolve_org_id(org_id)
36-
if resolved_org_id is None:
37-
console.print("[red]Error:[/red] Organization ID not provided. Pass --org-id, set CODEGEN_ORG_ID, or REPOSITORY_ORG_ID.")
38-
raise typer.Exit(1)
39-
85+
4086
console.print("🚀 Starting Claude Code with session tracking...", style="blue")
4187
console.print(f"🎯 Organization ID: {resolved_org_id}", style="dim")
4288

@@ -79,29 +125,27 @@ def claude(
79125
url = get_codegen_url(session_id)
80126
console.print(f"\n🔵 Codegen URL: {url}\n", style="bold blue")
81127

82-
83128
process = subprocess.Popen(["claude", "--session-id", session_id])
84129

85-
86130
# Start log watcher for the session
87131
console.print("📋 Starting log watcher...", style="blue")
88132
log_watcher_started = log_watcher_manager.start_watcher(
89133
session_id=session_id,
90134
org_id=resolved_org_id,
91-
poll_interval=1.0, # Check every second
92-
on_log_entry=None
135+
poll_interval=1.0,
136+
on_log_entry=None,
93137
)
94-
138+
95139
if not log_watcher_started:
96140
console.print("⚠️ Failed to start log watcher", style="yellow")
97141

98142
# Handle Ctrl+C gracefully
99143
def signal_handler(signum, frame):
100144
console.print("\n🛑 Stopping Claude Code...", style="yellow")
101-
log_watcher_manager.stop_all_watchers() # Stop log watchers
145+
log_watcher_manager.stop_all_watchers()
102146
process.terminate()
103-
cleanup_claude_hook() # Clean up our hook
104-
cleanup_codegen_mcp_server() # Clean up MCP Server
147+
cleanup_claude_hook()
148+
cleanup_codegen_mcp_server()
105149
end_claude_session(session_id, "ERROR", resolved_org_id)
106150
sys.exit(0)
107151

@@ -140,12 +184,33 @@ def signal_handler(signum, frame):
140184
log_watcher_manager.stop_all_watchers()
141185
except Exception as e:
142186
console.print(f"⚠️ Error stopping log watchers: {e}", style="yellow")
143-
187+
144188
cleanup_claude_hook()
145189

146190
# Show final session info
147191
url = get_codegen_url(session_id)
148192
console.print(f"\n🔵 Session URL: {url}", style="bold blue")
149193
console.print(f"🆔 Session ID: {session_id}", style="dim")
150194
console.print(f"🎯 Organization ID: {resolved_org_id}", style="dim")
151-
console.print("💡 Check your backend to see the session data", style="dim")
195+
console.print("💡 Check your backend to see the session data", style="dim")
196+
197+
198+
def claude(
199+
org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)"),
200+
no_mcp: bool | None = typer.Option(False, "--no-mcp", help="Disable Codegen's MCP server with additional capabilities over HTTP"),
201+
background: str | None = typer.Option(None, "--background", "-b", help="Create a background agent run with this prompt instead of launching Claude Code"),
202+
):
203+
"""Run Claude Code with session tracking or create a background run."""
204+
# Resolve org_id early for session management
205+
resolved_org_id = resolve_org_id(org_id)
206+
if resolved_org_id is None:
207+
console.print("[red]Error:[/red] Organization ID not provided. Pass --org-id, set CODEGEN_ORG_ID, or REPOSITORY_ORG_ID.")
208+
raise typer.Exit(1)
209+
210+
if background is not None:
211+
# Use the value from --background as the prompt, with --prompt as fallback
212+
final_prompt = background or prompt
213+
_run_claude_background(resolved_org_id, final_prompt)
214+
return
215+
216+
_run_claude_interactive(resolved_org_id, no_mcp)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Organization management command."""
2+
3+
from .main import org
4+
5+
__all__ = ["org"]

0 commit comments

Comments
 (0)