codee-agent 0.3.0__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codee_agent-0.3.0 → codee_agent-0.4.1}/PKG-INFO +1 -1
- {codee_agent-0.3.0 → codee_agent-0.4.1}/pyproject.toml +7 -1
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/admin.py +398 -26
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/admin_cli.py +12 -2
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/admin_service.py +387 -15
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/executor.py +4 -20
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/init_cli.py +5 -3
- codee_agent-0.4.1/src/codee/lib/mcp_config.py +112 -0
- codee_agent-0.4.1/src/codee/lib/test_mcp_config.py +134 -0
- codee_agent-0.4.1/src/codee/tasks_providers.py +25 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/test_admin_cli.py +32 -5
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/test_admin_service.py +498 -1
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/test_executor.py +3 -3
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/test_init_cli.py +7 -5
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/test_start_cli.py +2 -1
- codee_agent-0.4.1/src/codee_tasks_abstract/provider.py +151 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_tasks_azure_devops/oauth.py +17 -1
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_tasks_azure_devops/provider.py +95 -5
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_tasks_azure_devops/test.py +111 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_tasks_jira/provider.py +92 -13
- codee_agent-0.4.1/src/codee_tasks_jira/test.py +213 -0
- codee_agent-0.3.0/src/codee_tasks_abstract/provider.py +0 -58
- codee_agent-0.3.0/src/codee_tasks_jira/test.py +0 -75
- {codee_agent-0.3.0 → codee_agent-0.4.1}/LICENSE +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/README.md +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/.gitignore +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/__init__.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/admin_api.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/lib/__init__.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/lib/cron_describe.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/lib/runs_db.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/lib/test_runs_db.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/lib/test_trigger_cron_skills.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/lib/test_trigger_issue_skills.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/lib/trigger_aws_sqs_skills.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/lib/trigger_cron_skills.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/lib/trigger_email_skills.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/lib/trigger_issue_skills.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/mail_server.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/start_cli.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/templates/AGENTS.md +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/templates/CLAUDE.md +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/test_admin_api.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/test_memory_index.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee/workflow_graph.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_admin/__init__.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_admin/codee_admin.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_agent_abstract/__init__.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_agent_abstract/provider.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_agent_claude_code/__init__.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_agent_claude_code/provider.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_agent_github_copilot/__init__.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_agent_github_copilot/provider.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_agent_github_copilot/test.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_database/__init__.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_database/database.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_database/oauth_tokens.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_main_context/__init__.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_main_context/context.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_main_context/logging.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_main_context/test_logging.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_tasks_abstract/__init__.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_tasks_azure_devops/__init__.py +0 -0
- {codee_agent-0.3.0 → codee_agent-0.4.1}/src/codee_tasks_jira/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codee-agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: A virtual co-worker that picks up Jira and Azure DevOps tasks and solves them with coding agents.
|
|
5
5
|
Keywords: ai,agent,jira,azure-devops,claude-code,github-copilot,automation
|
|
6
6
|
Author: Denis Kibalko
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "codee-agent"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.1"
|
|
4
4
|
description = "A virtual co-worker that picks up Jira and Azure DevOps tasks and solves them with coding agents."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -47,6 +47,12 @@ codee-init = "codee.init_cli:main"
|
|
|
47
47
|
codee-admin = "codee.admin_cli:main"
|
|
48
48
|
codee-executor = "codee.executor:main"
|
|
49
49
|
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
# The provider and agent packages keep their suites in `test.py`, which pytest's
|
|
52
|
+
# default `test_*.py` pattern skips — without this, `pytest src` silently ran
|
|
53
|
+
# 61 fewer tests than the repo has.
|
|
54
|
+
python_files = ["test_*.py", "test.py"]
|
|
55
|
+
|
|
50
56
|
[tool.uv.build-backend]
|
|
51
57
|
module-name = [
|
|
52
58
|
"codee",
|
|
@@ -8,7 +8,8 @@ import reflex as rx
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
10
10
|
from codee.admin_api import api_app
|
|
11
|
-
from codee.admin_service import
|
|
11
|
+
from codee.admin_service import (
|
|
12
|
+
AGENTS_FILE, TASKS_CHECKS, AdminService, ISSUE_TYPES, SKILL_TYPES)
|
|
12
13
|
from codee.workflow_graph import workflow_graph
|
|
13
14
|
|
|
14
15
|
SERVICE = AdminService()
|
|
@@ -48,12 +49,47 @@ class MemoryEntry(BaseModel):
|
|
|
48
49
|
matched: bool
|
|
49
50
|
|
|
50
51
|
|
|
52
|
+
class RepositorySummary(BaseModel):
|
|
53
|
+
name: str
|
|
54
|
+
url: str
|
|
55
|
+
default_branch: str
|
|
56
|
+
|
|
57
|
+
|
|
51
58
|
class ActiveJob(BaseModel):
|
|
52
59
|
message: str
|
|
53
60
|
elapsed_label: str
|
|
54
61
|
viewer_url: str
|
|
55
62
|
|
|
56
63
|
|
|
64
|
+
class CheckResult(BaseModel):
|
|
65
|
+
"""One line of the tasks-provider check list on the settings page.
|
|
66
|
+
|
|
67
|
+
``status`` is where it is rather than how it went: a check has to be on
|
|
68
|
+
screen before it has an answer, so "waiting" and "running" are states of the
|
|
69
|
+
same row that later becomes ``ok`` or ``failed``.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
name: str
|
|
73
|
+
status: str
|
|
74
|
+
message: str = ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def pending_checks(done: list[CheckResult]) -> list[CheckResult]:
|
|
78
|
+
"""The finished checks, then the running one, then the ones still to come."""
|
|
79
|
+
rows = list(done)
|
|
80
|
+
for position, name in enumerate(TASKS_CHECKS[len(done):], start=len(done)):
|
|
81
|
+
rows.append(CheckResult(
|
|
82
|
+
name=name,
|
|
83
|
+
status="running" if position == len(done) else "waiting"))
|
|
84
|
+
return rows
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def check_result(check: dict[str, Any]) -> CheckResult:
|
|
88
|
+
return CheckResult(name=check["name"],
|
|
89
|
+
status="ok" if check["ok"] else "failed",
|
|
90
|
+
message=check["message"])
|
|
91
|
+
|
|
92
|
+
|
|
57
93
|
class RunRecord(BaseModel):
|
|
58
94
|
skill_name: str
|
|
59
95
|
trigger_type: str
|
|
@@ -95,6 +131,10 @@ class AdminState(rx.State):
|
|
|
95
131
|
selected_memory: str = ""
|
|
96
132
|
memory_content: str = ""
|
|
97
133
|
|
|
134
|
+
repositories: list[RepositorySummary] = []
|
|
135
|
+
new_repository_url: str = ""
|
|
136
|
+
adding_repository: bool = False
|
|
137
|
+
|
|
98
138
|
active_jobs: list[ActiveJob] = []
|
|
99
139
|
total_runs: int = 0
|
|
100
140
|
last_24h_runs: int = 0
|
|
@@ -133,6 +173,11 @@ class AdminState(rx.State):
|
|
|
133
173
|
azure_account: str = ""
|
|
134
174
|
azure_expires_label: str = ""
|
|
135
175
|
azure_redirect_uri: str = ""
|
|
176
|
+
tasks_verifying: bool = False
|
|
177
|
+
tasks_checks: list[CheckResult] = []
|
|
178
|
+
mcp_setup_ok: bool = False
|
|
179
|
+
mcp_setup_message: str = ""
|
|
180
|
+
mcp_configured: bool = False
|
|
136
181
|
|
|
137
182
|
@rx.var
|
|
138
183
|
def filtered_skills(self) -> list[SkillSummary]:
|
|
@@ -325,6 +370,11 @@ class AdminState(rx.State):
|
|
|
325
370
|
self.agents_content = SERVICE.load_agents()
|
|
326
371
|
self.editing_agents = True
|
|
327
372
|
|
|
373
|
+
def open_agents_editor(self) -> Any:
|
|
374
|
+
"""Open the AGENTS.md editor from a page that isn't Skills."""
|
|
375
|
+
self.edit_agents()
|
|
376
|
+
return rx.redirect("/skills")
|
|
377
|
+
|
|
328
378
|
def set_agents_content(self, value: str) -> None:
|
|
329
379
|
self.agents_content = value
|
|
330
380
|
|
|
@@ -335,7 +385,7 @@ class AdminState(rx.State):
|
|
|
335
385
|
return _save_toast(*SERVICE.save_agents(self.agents_content))
|
|
336
386
|
|
|
337
387
|
def load_memories(self) -> None:
|
|
338
|
-
self.
|
|
388
|
+
self.load_coding_agent()
|
|
339
389
|
if self.coding_agent == "github_copilot":
|
|
340
390
|
self.memories = []
|
|
341
391
|
self.selected_memory = ""
|
|
@@ -367,6 +417,33 @@ class AdminState(rx.State):
|
|
|
367
417
|
self.load_memories()
|
|
368
418
|
return _save_toast(deleted, pushed, message)
|
|
369
419
|
|
|
420
|
+
def load_repositories(self) -> None:
|
|
421
|
+
self.repositories = [RepositorySummary(**repository)
|
|
422
|
+
for repository in SERVICE.list_repositories()]
|
|
423
|
+
|
|
424
|
+
def set_new_repository_url(self, value: str) -> None:
|
|
425
|
+
self.new_repository_url = value
|
|
426
|
+
|
|
427
|
+
@rx.event(background=True)
|
|
428
|
+
async def add_repository(self) -> Any:
|
|
429
|
+
"""Clone in the background: a first clone can take minutes."""
|
|
430
|
+
async with self:
|
|
431
|
+
if self.adding_repository:
|
|
432
|
+
return
|
|
433
|
+
url = self.new_repository_url.strip()
|
|
434
|
+
self.adding_repository = True
|
|
435
|
+
try:
|
|
436
|
+
added, message, _ = await asyncio.to_thread(
|
|
437
|
+
SERVICE.add_repository, url)
|
|
438
|
+
except Exception as error:
|
|
439
|
+
added, message = False, str(error)
|
|
440
|
+
async with self:
|
|
441
|
+
self.adding_repository = False
|
|
442
|
+
if added:
|
|
443
|
+
self.new_repository_url = ""
|
|
444
|
+
self.load_repositories()
|
|
445
|
+
yield rx.toast.success(message) if added else rx.toast.error(message)
|
|
446
|
+
|
|
370
447
|
def _refresh_dashboard(self) -> None:
|
|
371
448
|
dashboard = SERVICE.dashboard()
|
|
372
449
|
self.active_jobs = [
|
|
@@ -476,6 +553,14 @@ class AdminState(rx.State):
|
|
|
476
553
|
self.edit_skill(slug)
|
|
477
554
|
return rx.redirect("/skills")
|
|
478
555
|
|
|
556
|
+
def load_coding_agent(self) -> None:
|
|
557
|
+
"""Refresh the agent the sidebar keys off, on every page load.
|
|
558
|
+
|
|
559
|
+
Without this the default sticks until a page that reads settings is
|
|
560
|
+
opened, so the Sessions link shows for Github Copilot.
|
|
561
|
+
"""
|
|
562
|
+
self.coding_agent = SERVICE.load_settings().coding_agent.value
|
|
563
|
+
|
|
479
564
|
def load_settings(self) -> Any:
|
|
480
565
|
settings = SERVICE.load_settings()
|
|
481
566
|
self.tasks_provider = settings.tasks_provider.value
|
|
@@ -491,9 +576,15 @@ class AdminState(rx.State):
|
|
|
491
576
|
self.azure_tenant_id = azure.get("tenant_id", "")
|
|
492
577
|
self.azure_client_id = azure.get("client_id", "")
|
|
493
578
|
self.azure_client_secret = azure.get("client_secret", "")
|
|
579
|
+
self._drop_provider_results()
|
|
580
|
+
self.load_mcp_status()
|
|
494
581
|
self.load_azure_connection()
|
|
495
582
|
return self._azure_callback_toast()
|
|
496
583
|
|
|
584
|
+
def load_mcp_status(self) -> None:
|
|
585
|
+
"""Read back whether the selected provider's MCP server is already installed."""
|
|
586
|
+
self.mcp_configured = SERVICE.tasks_mcp_configured(self.tasks_provider)
|
|
587
|
+
|
|
497
588
|
def load_azure_connection(self) -> None:
|
|
498
589
|
connection = SERVICE.azure_connection()
|
|
499
590
|
self.azure_connected = connection["connected"]
|
|
@@ -512,8 +603,17 @@ class AdminState(rx.State):
|
|
|
512
603
|
return rx.toast.success(message or "Connected to Azure DevOps")
|
|
513
604
|
return rx.toast.error(message or "Could not connect to Azure DevOps")
|
|
514
605
|
|
|
606
|
+
def _drop_provider_results(self) -> None:
|
|
607
|
+
"""Forget the last checks and MCP setup: they spoke for credentials that just changed."""
|
|
608
|
+
self.tasks_verifying = False
|
|
609
|
+
self.tasks_checks = []
|
|
610
|
+
self.mcp_setup_ok = False
|
|
611
|
+
self.mcp_setup_message = ""
|
|
612
|
+
|
|
515
613
|
def set_tasks_provider(self, value: str) -> None:
|
|
516
614
|
self.tasks_provider = value
|
|
615
|
+
self._drop_provider_results()
|
|
616
|
+
self.load_mcp_status()
|
|
517
617
|
|
|
518
618
|
def set_coding_agent(self, value: str) -> None:
|
|
519
619
|
self.coding_agent = value
|
|
@@ -523,27 +623,35 @@ class AdminState(rx.State):
|
|
|
523
623
|
|
|
524
624
|
def set_jira_base_url(self, value: str) -> None:
|
|
525
625
|
self.jira_base_url = value
|
|
626
|
+
self._drop_provider_results()
|
|
526
627
|
|
|
527
628
|
def set_jira_account_email(self, value: str) -> None:
|
|
528
629
|
self.jira_account_email = value
|
|
630
|
+
self._drop_provider_results()
|
|
529
631
|
|
|
530
632
|
def set_jira_api_token(self, value: str) -> None:
|
|
531
633
|
self.jira_api_token = value
|
|
634
|
+
self._drop_provider_results()
|
|
532
635
|
|
|
533
636
|
def set_jira_project(self, value: str) -> None:
|
|
534
637
|
self.jira_project = value
|
|
638
|
+
self._drop_provider_results()
|
|
535
639
|
|
|
536
640
|
def set_azure_organization_url(self, value: str) -> None:
|
|
537
641
|
self.azure_organization_url = value
|
|
642
|
+
self._drop_provider_results()
|
|
538
643
|
|
|
539
644
|
def set_azure_tenant_id(self, value: str) -> None:
|
|
540
645
|
self.azure_tenant_id = value
|
|
646
|
+
self._drop_provider_results()
|
|
541
647
|
|
|
542
648
|
def set_azure_client_id(self, value: str) -> None:
|
|
543
649
|
self.azure_client_id = value
|
|
650
|
+
self._drop_provider_results()
|
|
544
651
|
|
|
545
652
|
def set_azure_client_secret(self, value: str) -> None:
|
|
546
653
|
self.azure_client_secret = value
|
|
654
|
+
self._drop_provider_results()
|
|
547
655
|
|
|
548
656
|
@rx.var
|
|
549
657
|
def azure_can_connect(self) -> bool:
|
|
@@ -552,6 +660,101 @@ class AdminState(rx.State):
|
|
|
552
660
|
self.azure_organization_url,
|
|
553
661
|
self.azure_client_id, self.azure_client_secret))
|
|
554
662
|
|
|
663
|
+
@rx.var
|
|
664
|
+
def tasks_can_verify(self) -> bool:
|
|
665
|
+
"""Whether a real pull is worth attempting with what's on the form.
|
|
666
|
+
|
|
667
|
+
Every field the provider reads has to be filled in, and Azure DevOps
|
|
668
|
+
additionally needs the OAuth consent — without it the check could only
|
|
669
|
+
ever report "not connected", which the status line above already says.
|
|
670
|
+
"""
|
|
671
|
+
if self.tasks_provider == "jira":
|
|
672
|
+
return all(value.strip() for value in (
|
|
673
|
+
self.jira_base_url, self.jira_account_email,
|
|
674
|
+
self.jira_api_token, self.jira_project))
|
|
675
|
+
return self.azure_can_connect and self.azure_connected
|
|
676
|
+
|
|
677
|
+
@rx.var
|
|
678
|
+
def mcp_can_setup(self) -> bool:
|
|
679
|
+
"""Whether the selected provider can describe its MCP server yet.
|
|
680
|
+
|
|
681
|
+
Less than the pull needs in both cases. mcp-atlassian doesn't take a
|
|
682
|
+
project key — the agent searches and updates issues wherever they live
|
|
683
|
+
rather than polling one project — and the Azure DevOps server takes only
|
|
684
|
+
the organization, since it signs in through the Azure CLI rather than
|
|
685
|
+
through the app registration.
|
|
686
|
+
"""
|
|
687
|
+
if self.tasks_provider == "jira":
|
|
688
|
+
return all(value.strip() for value in (
|
|
689
|
+
self.jira_base_url, self.jira_account_email,
|
|
690
|
+
self.jira_api_token))
|
|
691
|
+
return bool(self.azure_organization_url.strip())
|
|
692
|
+
|
|
693
|
+
@rx.var
|
|
694
|
+
def mcp_provider_label(self) -> str:
|
|
695
|
+
return ("Azure DevOps" if self.tasks_provider == "azure_devops"
|
|
696
|
+
else "Jira")
|
|
697
|
+
|
|
698
|
+
@rx.var
|
|
699
|
+
def mcp_button_label(self) -> str:
|
|
700
|
+
"""Re-running is how stale credentials in the file get replaced."""
|
|
701
|
+
return (f"Setup {self.mcp_provider_label} MCP again"
|
|
702
|
+
if self.mcp_configured
|
|
703
|
+
else f"Setup {self.mcp_provider_label} MCP")
|
|
704
|
+
|
|
705
|
+
@rx.var
|
|
706
|
+
def mcp_missing_hint(self) -> str:
|
|
707
|
+
if self.tasks_provider == "jira":
|
|
708
|
+
return "Fill in base URL, account email and API token first."
|
|
709
|
+
return "Fill in the organization URL first."
|
|
710
|
+
|
|
711
|
+
def setup_tasks_mcp(self) -> None:
|
|
712
|
+
"""Write the provider's MCP server into the project's .mcp.json.
|
|
713
|
+
|
|
714
|
+
Synchronous, unlike the connection check: this only touches a local file,
|
|
715
|
+
and the result should be on screen by the time the click lands.
|
|
716
|
+
"""
|
|
717
|
+
self.mcp_setup_ok, self.mcp_setup_message = SERVICE.setup_tasks_mcp(
|
|
718
|
+
self.tasks_provider, self._credentials())
|
|
719
|
+
self.load_mcp_status()
|
|
720
|
+
|
|
721
|
+
@rx.event(background=True)
|
|
722
|
+
async def verify_tasks_connection(self) -> Any:
|
|
723
|
+
"""Run the provider checks with the credentials on the form.
|
|
724
|
+
|
|
725
|
+
In the background because none of it is quick: the provider allows
|
|
726
|
+
itself 30s per request, and the MCP check runs a whole coding agent. The
|
|
727
|
+
whole list goes up at once and each row is filled in as its check
|
|
728
|
+
finishes, so the wait is spent looking at what is still running rather
|
|
729
|
+
than at nothing. Nothing is saved — verifying shouldn't commit anything.
|
|
730
|
+
"""
|
|
731
|
+
async with self:
|
|
732
|
+
if self.tasks_verifying or not self.tasks_can_verify:
|
|
733
|
+
return
|
|
734
|
+
self.tasks_verifying = True
|
|
735
|
+
self.tasks_checks = pending_checks([])
|
|
736
|
+
provider, credentials = self.tasks_provider, self._credentials()
|
|
737
|
+
|
|
738
|
+
# A generator, so nothing has run yet: each next() is one check, and the
|
|
739
|
+
# thread keeps the page responsive while it does.
|
|
740
|
+
checks = SERVICE.verify_tasks_connection(provider, credentials)
|
|
741
|
+
done: list[CheckResult] = []
|
|
742
|
+
failure = ""
|
|
743
|
+
try:
|
|
744
|
+
while (result := await asyncio.to_thread(next, checks, None)) is not None:
|
|
745
|
+
done.append(check_result(result))
|
|
746
|
+
async with self:
|
|
747
|
+
self.tasks_checks = pending_checks(done)
|
|
748
|
+
except Exception as error:
|
|
749
|
+
failure = str(error)
|
|
750
|
+
async with self:
|
|
751
|
+
self.tasks_verifying = False
|
|
752
|
+
# Whatever the generator stopped short of is dropped rather than
|
|
753
|
+
# left spinning: a run that yielded one check has only one to show.
|
|
754
|
+
self.tasks_checks = done + ([CheckResult(
|
|
755
|
+
name="Verification failed", status="failed", message=failure)]
|
|
756
|
+
if failure else [])
|
|
757
|
+
|
|
555
758
|
def connect_azure_devops(self) -> Any:
|
|
556
759
|
"""Save the app registration, then hand the browser to Entra ID for consent.
|
|
557
760
|
|
|
@@ -572,35 +775,37 @@ class AdminState(rx.State):
|
|
|
572
775
|
|
|
573
776
|
def disconnect_azure_devops(self) -> Any:
|
|
574
777
|
SERVICE.disconnect_azure()
|
|
778
|
+
self._drop_provider_results()
|
|
575
779
|
self.load_azure_connection()
|
|
576
780
|
return rx.toast.success("Disconnected from Azure DevOps")
|
|
577
781
|
|
|
782
|
+
def _credentials(self) -> dict[str, str]:
|
|
783
|
+
"""The selected provider's credentials as they stand on the form."""
|
|
784
|
+
if self.tasks_provider == "jira":
|
|
785
|
+
return {
|
|
786
|
+
"base_url": self.jira_base_url,
|
|
787
|
+
"account_email": self.jira_account_email,
|
|
788
|
+
"api_token": self.jira_api_token,
|
|
789
|
+
"project": self.jira_project,
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
"organization_url": self.azure_organization_url,
|
|
793
|
+
"tenant_id": self.azure_tenant_id,
|
|
794
|
+
"client_id": self.azure_client_id,
|
|
795
|
+
"client_secret": self.azure_client_secret,
|
|
796
|
+
}
|
|
797
|
+
|
|
578
798
|
def _persist_settings(self) -> str:
|
|
579
799
|
"""Write the settings to disk. Returns an error message, or '' when saved."""
|
|
580
800
|
try:
|
|
581
801
|
parallel_agents = int(self.max_parallel_agents)
|
|
582
802
|
except ValueError:
|
|
583
803
|
return "Max parallel tasks must be a number"
|
|
584
|
-
credentials = (
|
|
585
|
-
{
|
|
586
|
-
"base_url": self.jira_base_url,
|
|
587
|
-
"account_email": self.jira_account_email,
|
|
588
|
-
"api_token": self.jira_api_token,
|
|
589
|
-
"project": self.jira_project,
|
|
590
|
-
}
|
|
591
|
-
if self.tasks_provider == "jira"
|
|
592
|
-
else {
|
|
593
|
-
"organization_url": self.azure_organization_url,
|
|
594
|
-
"tenant_id": self.azure_tenant_id,
|
|
595
|
-
"client_id": self.azure_client_id,
|
|
596
|
-
"client_secret": self.azure_client_secret,
|
|
597
|
-
}
|
|
598
|
-
)
|
|
599
804
|
SERVICE.save_settings(
|
|
600
805
|
self.tasks_provider,
|
|
601
806
|
self.coding_agent,
|
|
602
807
|
parallel_agents,
|
|
603
|
-
|
|
808
|
+
self._credentials(),
|
|
604
809
|
)
|
|
605
810
|
return ""
|
|
606
811
|
|
|
@@ -682,6 +887,8 @@ def shell(content: rx.Component) -> rx.Component:
|
|
|
682
887
|
nav_link("Skills", "blocks", "/skills"),
|
|
683
888
|
nav_link("Workflow", "git-branch", "/workflow"),
|
|
684
889
|
nav_link("Memory", "notebook-text", "/memory"),
|
|
890
|
+
nav_link("Repositories", "folder-git-2",
|
|
891
|
+
"/repositories"),
|
|
685
892
|
nav_link("Runs", "history", "/runs"),
|
|
686
893
|
rx.cond(
|
|
687
894
|
AdminState.coding_agent == "claude_code",
|
|
@@ -762,7 +969,8 @@ def live_dot(size: str = "0.6rem") -> rx.Component:
|
|
|
762
969
|
return rx.box(
|
|
763
970
|
rx.box(position="absolute", inset="0", border_radius="50%", background=ACCENT,
|
|
764
971
|
animation="codee-ping 1.8s cubic-bezier(0, 0, 0.2, 1) infinite"),
|
|
765
|
-
rx.box(position="absolute", inset="0",
|
|
972
|
+
rx.box(position="absolute", inset="0",
|
|
973
|
+
border_radius="50%", background=ACCENT),
|
|
766
974
|
class_name="codee-live-dot",
|
|
767
975
|
position="relative",
|
|
768
976
|
width=size,
|
|
@@ -774,7 +982,8 @@ def live_dot(size: str = "0.6rem") -> rx.Component:
|
|
|
774
982
|
def elapsed_pill(label: rx.Var | str) -> rx.Component:
|
|
775
983
|
return rx.hstack(
|
|
776
984
|
rx.icon("timer", size=14, color=ACCENT),
|
|
777
|
-
rx.text(label, font_family=MONO,
|
|
985
|
+
rx.text(label, font_family=MONO,
|
|
986
|
+
font_size="0.85rem", font_weight="500"),
|
|
778
987
|
spacing="2",
|
|
779
988
|
align="center",
|
|
780
989
|
flex_shrink="0",
|
|
@@ -1201,7 +1410,7 @@ def memory_page() -> rx.Component:
|
|
|
1201
1410
|
rx.vstack(rx.foreach(AdminState.memories,
|
|
1202
1411
|
memory_row), spacing="3", width="100%"),
|
|
1203
1412
|
empty_state("notebook-text", "No memories yet."))
|
|
1204
|
-
return shell(rx.vstack(page_header("Memory", "
|
|
1413
|
+
return shell(rx.vstack(page_header("Memory", "Manage agent provider's memory."),
|
|
1205
1414
|
rx.cond(AdminState.coding_agent == "github_copilot",
|
|
1206
1415
|
empty_state(
|
|
1207
1416
|
"notebook-text",
|
|
@@ -1211,6 +1420,63 @@ def memory_page() -> rx.Component:
|
|
|
1211
1420
|
align="start", width="100%"))
|
|
1212
1421
|
|
|
1213
1422
|
|
|
1423
|
+
def repository_row(repository: RepositorySummary) -> rx.Component:
|
|
1424
|
+
return rx.flex(
|
|
1425
|
+
rx.icon("folder-git-2", size=18, color=SUBTLE_ICON),
|
|
1426
|
+
rx.vstack(
|
|
1427
|
+
rx.hstack(rx.text(repository.name, font_weight="600"),
|
|
1428
|
+
rx.cond(repository.default_branch != "",
|
|
1429
|
+
rx.badge(rx.icon("house", size=12),
|
|
1430
|
+
repository.default_branch,
|
|
1431
|
+
color_scheme="blue", variant="soft",
|
|
1432
|
+
custom_attrs={"title": "Default branch"})),
|
|
1433
|
+
spacing="2", align="center"),
|
|
1434
|
+
rx.cond(repository.url != "",
|
|
1435
|
+
rx.text(repository.url, color=MUTED, font_family=MONO,
|
|
1436
|
+
font_size="0.8rem")),
|
|
1437
|
+
spacing="2", align="start", flex="1", min_width="0"),
|
|
1438
|
+
gap="0.85rem", align="start", padding="1rem", background=SURFACE,
|
|
1439
|
+
border=BORDER, width="100%")
|
|
1440
|
+
|
|
1441
|
+
|
|
1442
|
+
def repositories_page() -> rx.Component:
|
|
1443
|
+
listing = rx.cond(
|
|
1444
|
+
AdminState.repositories.length() > 0,
|
|
1445
|
+
rx.vstack(rx.foreach(AdminState.repositories, repository_row),
|
|
1446
|
+
spacing="3", width="100%"),
|
|
1447
|
+
empty_state("folder-git-2", "No repositories cloned yet."))
|
|
1448
|
+
return shell(rx.vstack(
|
|
1449
|
+
page_header("Repositories",
|
|
1450
|
+
"Repositories the coding agents work in."),
|
|
1451
|
+
rx.box(
|
|
1452
|
+
rx.flex(
|
|
1453
|
+
rx.input(placeholder="git@github.com:org/repo.git",
|
|
1454
|
+
value=AdminState.new_repository_url,
|
|
1455
|
+
on_change=AdminState.set_new_repository_url,
|
|
1456
|
+
flex="1"),
|
|
1457
|
+
rx.button(rx.icon("plus", size=16), "Add repository",
|
|
1458
|
+
loading=AdminState.adding_repository,
|
|
1459
|
+
on_click=AdminState.add_repository),
|
|
1460
|
+
gap="0.75rem", width="100%"),
|
|
1461
|
+
rx.text("Clones a bare repository into repositories/<name>/.bare "
|
|
1462
|
+
"and checks out its default branch as a worktree beside it. "
|
|
1463
|
+
"A first clone can take a few minutes.",
|
|
1464
|
+
color=MUTED, font_size="0.82rem", margin_top="0.75rem"),
|
|
1465
|
+
padding="1.25rem", background=SURFACE, border=BORDER, width="100%"),
|
|
1466
|
+
rx.callout(
|
|
1467
|
+
rx.fragment(
|
|
1468
|
+
"The agents work better when they know what each repository "
|
|
1469
|
+
"is for: describe them in ",
|
|
1470
|
+
rx.link(AGENTS_FILE, on_click=AdminState.open_agents_editor,
|
|
1471
|
+
color=ACCENT, cursor="pointer",
|
|
1472
|
+
text_decoration="underline"),
|
|
1473
|
+
".",
|
|
1474
|
+
),
|
|
1475
|
+
icon="info", size="1", color_scheme="gray", width="100%"),
|
|
1476
|
+
listing,
|
|
1477
|
+
spacing="5", align="start", width="100%"))
|
|
1478
|
+
|
|
1479
|
+
|
|
1214
1480
|
def run_row(run: RunRecord) -> rx.Component:
|
|
1215
1481
|
return rx.box(
|
|
1216
1482
|
rx.flex(
|
|
@@ -1516,6 +1782,105 @@ def azure_fields() -> rx.Component:
|
|
|
1516
1782
|
spacing="4", width="100%")
|
|
1517
1783
|
|
|
1518
1784
|
|
|
1785
|
+
def mcp_setup_button() -> rx.Component:
|
|
1786
|
+
"""The setup button, built fresh per branch so each can sit in its own row."""
|
|
1787
|
+
return rx.button(
|
|
1788
|
+
rx.icon("file-cog", size=16), AdminState.mcp_button_label,
|
|
1789
|
+
type="button", variant="outline",
|
|
1790
|
+
disabled=~AdminState.mcp_can_setup,
|
|
1791
|
+
on_click=AdminState.setup_tasks_mcp)
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
def tasks_mcp_setup() -> rx.Component:
|
|
1795
|
+
"""Hand the provider's credentials to the coding agent as an MCP server."""
|
|
1796
|
+
return rx.vstack(
|
|
1797
|
+
rx.cond(
|
|
1798
|
+
AdminState.mcp_configured,
|
|
1799
|
+
# Already installed: the state is the sentence, and the button that
|
|
1800
|
+
# rewrites it with whatever the fields now say sits beside it.
|
|
1801
|
+
rx.hstack(
|
|
1802
|
+
rx.icon("circle-check", size=16, color="var(--green-9)"),
|
|
1803
|
+
rx.text("MCP config already set up.", font_size="0.85rem"),
|
|
1804
|
+
mcp_setup_button(),
|
|
1805
|
+
spacing="3", align="center", width="100%"),
|
|
1806
|
+
rx.hstack(
|
|
1807
|
+
mcp_setup_button(),
|
|
1808
|
+
rx.cond(
|
|
1809
|
+
AdminState.mcp_can_setup,
|
|
1810
|
+
rx.text("Writes .mcp.json so the coding agent can read and "
|
|
1811
|
+
"update tasks itself.",
|
|
1812
|
+
color=MUTED, font_size="0.8rem"),
|
|
1813
|
+
rx.text(AdminState.mcp_missing_hint,
|
|
1814
|
+
color=MUTED, font_size="0.8rem")),
|
|
1815
|
+
spacing="3", align="center", width="100%")),
|
|
1816
|
+
rx.cond(
|
|
1817
|
+
AdminState.mcp_setup_message != "",
|
|
1818
|
+
rx.cond(
|
|
1819
|
+
AdminState.mcp_setup_ok,
|
|
1820
|
+
rx.callout(AdminState.mcp_setup_message, icon="circle-check",
|
|
1821
|
+
size="1", color_scheme="green", width="100%"),
|
|
1822
|
+
rx.callout(AdminState.mcp_setup_message, icon="circle-alert",
|
|
1823
|
+
size="1", color_scheme="red", width="100%")),
|
|
1824
|
+
),
|
|
1825
|
+
spacing="3", width="100%")
|
|
1826
|
+
|
|
1827
|
+
|
|
1828
|
+
def tasks_check_row(check: CheckResult) -> rx.Component:
|
|
1829
|
+
"""One check: where it stands as an icon, its name, and what it found."""
|
|
1830
|
+
marker = {"flex_shrink": "0", "margin_top": "0.15rem"}
|
|
1831
|
+
return rx.hstack(
|
|
1832
|
+
rx.match(
|
|
1833
|
+
check.status,
|
|
1834
|
+
("running", rx.spinner(size="2", **marker)),
|
|
1835
|
+
("ok", rx.icon("circle-check", size=16,
|
|
1836
|
+
color="var(--green-9)", **marker)),
|
|
1837
|
+
("failed", rx.icon("circle-alert", size=16,
|
|
1838
|
+
color="var(--red-9)", **marker)),
|
|
1839
|
+
# Still queued: named so the list is complete from the first frame,
|
|
1840
|
+
# but visibly not its turn yet.
|
|
1841
|
+
rx.icon("circle-dashed", size=16, color=MUTED, **marker)),
|
|
1842
|
+
rx.vstack(
|
|
1843
|
+
rx.text(check.name, font_size="0.85rem", weight="medium",
|
|
1844
|
+
color=rx.cond(check.status == "waiting", MUTED, TEXT)),
|
|
1845
|
+
rx.cond(
|
|
1846
|
+
check.message != "",
|
|
1847
|
+
rx.text(check.message, color=MUTED, font_size="0.8rem")),
|
|
1848
|
+
spacing="1", align="start", width="100%"),
|
|
1849
|
+
spacing="2", align="start", width="100%")
|
|
1850
|
+
|
|
1851
|
+
|
|
1852
|
+
def tasks_verification() -> rx.Component:
|
|
1853
|
+
"""Run the provider checks and print what each one found."""
|
|
1854
|
+
return rx.vstack(
|
|
1855
|
+
rx.hstack(
|
|
1856
|
+
rx.button(
|
|
1857
|
+
rx.cond(AdminState.tasks_verifying,
|
|
1858
|
+
rx.spinner(size="2"), rx.icon("plug-zap", size=16)),
|
|
1859
|
+
rx.cond(AdminState.tasks_verifying,
|
|
1860
|
+
"Verifying…", "Verify connection"),
|
|
1861
|
+
type="button", variant="outline",
|
|
1862
|
+
disabled=AdminState.tasks_verifying | ~AdminState.tasks_can_verify,
|
|
1863
|
+
on_click=AdminState.verify_tasks_connection),
|
|
1864
|
+
rx.cond(
|
|
1865
|
+
AdminState.tasks_can_verify,
|
|
1866
|
+
# Worth saying up front: the second check spends a coding-agent
|
|
1867
|
+
# run and leaves a closed task behind in the real backend.
|
|
1868
|
+
rx.text("Pulls tasks, then has the coding agent create and "
|
|
1869
|
+
"close one task through MCP. Takes a minute.",
|
|
1870
|
+
color=MUTED, font_size="0.8rem"),
|
|
1871
|
+
rx.text("Fill in every field above to check the connection.",
|
|
1872
|
+
color=MUTED, font_size="0.8rem")),
|
|
1873
|
+
spacing="3", align="center", width="100%"),
|
|
1874
|
+
rx.cond(
|
|
1875
|
+
AdminState.tasks_checks,
|
|
1876
|
+
rx.vstack(rx.foreach(AdminState.tasks_checks, tasks_check_row),
|
|
1877
|
+
spacing="3", width="100%",
|
|
1878
|
+
padding="0.85rem", border=BORDER,
|
|
1879
|
+
background=PAGE_BACKGROUND),
|
|
1880
|
+
),
|
|
1881
|
+
spacing="3", width="100%")
|
|
1882
|
+
|
|
1883
|
+
|
|
1519
1884
|
def settings_page() -> rx.Component:
|
|
1520
1885
|
jira_fields = rx.vstack(
|
|
1521
1886
|
field("Base URL", rx.input(value=AdminState.jira_base_url,
|
|
@@ -1529,7 +1894,7 @@ def settings_page() -> rx.Component:
|
|
|
1529
1894
|
spacing="4", width="100%")
|
|
1530
1895
|
return shell(rx.vstack(
|
|
1531
1896
|
page_header(
|
|
1532
|
-
"Settings", "
|
|
1897
|
+
"Settings", ""),
|
|
1533
1898
|
rx.box(
|
|
1534
1899
|
rx.heading("Coding agent", size="4", margin_bottom="1rem"),
|
|
1535
1900
|
rx.grid(
|
|
@@ -1546,6 +1911,8 @@ def settings_page() -> rx.Component:
|
|
|
1546
1911
|
on_change=AdminState.set_tasks_provider, width="100%")),
|
|
1547
1912
|
rx.box(rx.cond(AdminState.tasks_provider == "jira",
|
|
1548
1913
|
jira_fields, azure_fields()), margin_top="1rem"),
|
|
1914
|
+
rx.box(tasks_mcp_setup(), margin_top="1.25rem"),
|
|
1915
|
+
rx.box(tasks_verification(), margin_top="1.25rem"),
|
|
1549
1916
|
padding="1.25rem", background=SURFACE, border=BORDER, width="100%"),
|
|
1550
1917
|
rx.button(rx.icon("save", size=16), "Save settings",
|
|
1551
1918
|
on_click=AdminState.save_settings),
|
|
@@ -1665,15 +2032,20 @@ app = rx.App(
|
|
|
1665
2032
|
api_transformer=api_app,
|
|
1666
2033
|
)
|
|
1667
2034
|
app.add_page(dashboard_page, route="/", title="Dashboard | Codee",
|
|
1668
|
-
on_load=AdminState.poll_dashboard)
|
|
2035
|
+
on_load=[AdminState.load_coding_agent, AdminState.poll_dashboard])
|
|
1669
2036
|
app.add_page(skills_page, route="/skills", title="Skills | Codee",
|
|
1670
|
-
on_load=[AdminState.
|
|
2037
|
+
on_load=[AdminState.load_coding_agent, AdminState.load_skills,
|
|
2038
|
+
AdminState.load_agent_models])
|
|
1671
2039
|
app.add_page(workflow_page, route="/workflow", title="Workflow | Codee",
|
|
1672
|
-
on_load=AdminState.load_workflow)
|
|
2040
|
+
on_load=[AdminState.load_coding_agent, AdminState.load_workflow])
|
|
1673
2041
|
app.add_page(memory_page, route="/memory", title="Memory | Codee",
|
|
1674
2042
|
on_load=AdminState.load_memories)
|
|
2043
|
+
app.add_page(repositories_page, route="/repositories",
|
|
2044
|
+
title="Repositories | Codee",
|
|
2045
|
+
on_load=[AdminState.load_coding_agent,
|
|
2046
|
+
AdminState.load_repositories])
|
|
1675
2047
|
app.add_page(runs_page, route="/runs", title="Runs | Codee",
|
|
1676
|
-
on_load=AdminState.load_runs)
|
|
2048
|
+
on_load=[AdminState.load_coding_agent, AdminState.load_runs])
|
|
1677
2049
|
app.add_page(sessions_page, route="/sessions",
|
|
1678
2050
|
title="Sessions | Codee", on_load=AdminState.load_settings)
|
|
1679
2051
|
app.add_page(settings_page, route="/settings",
|
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
import sys
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
from codee.admin_service import DEFAULT_ADMIN_PORT
|
|
7
|
+
from codee.admin_service import DEFAULT_ADMIN_PORT, public_base_url
|
|
8
8
|
from codee_main_context.context import data_dir, project_root
|
|
9
9
|
from codee_main_context.logging import (
|
|
10
10
|
configure_logging, enable_debug, get_logger, verbose_debug_enabled)
|
|
@@ -34,6 +34,16 @@ config = rx.Config(
|
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def _api_url(port: int) -> str:
|
|
38
|
+
"""Origin the browser uses for the backend websocket and uploads.
|
|
39
|
+
|
|
40
|
+
Reflex bakes this into the compiled frontend, so behind a reverse proxy it
|
|
41
|
+
has to be the public origin rather than the loopback address the server
|
|
42
|
+
binds to -- otherwise the page loads and then fails to open its socket.
|
|
43
|
+
"""
|
|
44
|
+
return public_base_url() or f"http://127.0.0.1:{port}"
|
|
45
|
+
|
|
46
|
+
|
|
37
47
|
def _runtime_directory(root: Path) -> Path:
|
|
38
48
|
runtime = data_dir(root) / "reflex"
|
|
39
49
|
runtime.mkdir(parents=True, exist_ok=True)
|
|
@@ -59,7 +69,7 @@ def main() -> int:
|
|
|
59
69
|
codee_data = data_dir(root).resolve()
|
|
60
70
|
os.environ["CODEE_PROJECT_ROOT"] = str(root)
|
|
61
71
|
os.environ["CODEE_DATA_DIR"] = str(codee_data)
|
|
62
|
-
os.environ["REFLEX_API_URL"] =
|
|
72
|
+
os.environ["REFLEX_API_URL"] = _api_url(arguments.port)
|
|
63
73
|
os.chdir(_runtime_directory(root))
|
|
64
74
|
|
|
65
75
|
from reflex.reflex import cli
|