codee-agent 0.4.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.4.0 → codee_agent-0.4.1}/PKG-INFO +1 -1
- {codee_agent-0.4.0 → codee_agent-0.4.1}/pyproject.toml +7 -1
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/admin.py +273 -17
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/admin_cli.py +12 -2
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/admin_service.py +244 -15
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/executor.py +4 -20
- {codee_agent-0.4.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.4.0 → codee_agent-0.4.1}/src/codee/test_admin_cli.py +32 -5
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/test_admin_service.py +382 -1
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/test_executor.py +3 -3
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/test_init_cli.py +7 -5
- {codee_agent-0.4.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.4.0 → codee_agent-0.4.1}/src/codee_tasks_azure_devops/oauth.py +17 -1
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_tasks_azure_devops/provider.py +95 -5
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_tasks_azure_devops/test.py +111 -0
- {codee_agent-0.4.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.4.0/src/codee_tasks_abstract/provider.py +0 -58
- codee_agent-0.4.0/src/codee_tasks_jira/test.py +0 -75
- {codee_agent-0.4.0 → codee_agent-0.4.1}/LICENSE +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/README.md +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/.gitignore +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/__init__.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/admin_api.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/__init__.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/cron_describe.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/runs_db.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/test_runs_db.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/test_trigger_cron_skills.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/test_trigger_issue_skills.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/trigger_aws_sqs_skills.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/trigger_cron_skills.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/trigger_email_skills.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/trigger_issue_skills.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/mail_server.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/start_cli.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/AGENTS.md +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/CLAUDE.md +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/test_admin_api.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/test_memory_index.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/workflow_graph.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_admin/__init__.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_admin/codee_admin.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_agent_abstract/__init__.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_agent_abstract/provider.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_agent_claude_code/__init__.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_agent_claude_code/provider.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_agent_github_copilot/__init__.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_agent_github_copilot/provider.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_agent_github_copilot/test.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_database/__init__.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_database/database.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_database/oauth_tokens.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_main_context/__init__.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_main_context/context.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_main_context/logging.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_main_context/test_logging.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_tasks_abstract/__init__.py +0 -0
- {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_tasks_azure_devops/__init__.py +0 -0
- {codee_agent-0.4.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.4.
|
|
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.4.
|
|
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()
|
|
@@ -60,6 +61,35 @@ class ActiveJob(BaseModel):
|
|
|
60
61
|
viewer_url: str
|
|
61
62
|
|
|
62
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
|
+
|
|
63
93
|
class RunRecord(BaseModel):
|
|
64
94
|
skill_name: str
|
|
65
95
|
trigger_type: str
|
|
@@ -143,6 +173,11 @@ class AdminState(rx.State):
|
|
|
143
173
|
azure_account: str = ""
|
|
144
174
|
azure_expires_label: str = ""
|
|
145
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
|
|
146
181
|
|
|
147
182
|
@rx.var
|
|
148
183
|
def filtered_skills(self) -> list[SkillSummary]:
|
|
@@ -541,9 +576,15 @@ class AdminState(rx.State):
|
|
|
541
576
|
self.azure_tenant_id = azure.get("tenant_id", "")
|
|
542
577
|
self.azure_client_id = azure.get("client_id", "")
|
|
543
578
|
self.azure_client_secret = azure.get("client_secret", "")
|
|
579
|
+
self._drop_provider_results()
|
|
580
|
+
self.load_mcp_status()
|
|
544
581
|
self.load_azure_connection()
|
|
545
582
|
return self._azure_callback_toast()
|
|
546
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
|
+
|
|
547
588
|
def load_azure_connection(self) -> None:
|
|
548
589
|
connection = SERVICE.azure_connection()
|
|
549
590
|
self.azure_connected = connection["connected"]
|
|
@@ -562,8 +603,17 @@ class AdminState(rx.State):
|
|
|
562
603
|
return rx.toast.success(message or "Connected to Azure DevOps")
|
|
563
604
|
return rx.toast.error(message or "Could not connect to Azure DevOps")
|
|
564
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
|
+
|
|
565
613
|
def set_tasks_provider(self, value: str) -> None:
|
|
566
614
|
self.tasks_provider = value
|
|
615
|
+
self._drop_provider_results()
|
|
616
|
+
self.load_mcp_status()
|
|
567
617
|
|
|
568
618
|
def set_coding_agent(self, value: str) -> None:
|
|
569
619
|
self.coding_agent = value
|
|
@@ -573,27 +623,35 @@ class AdminState(rx.State):
|
|
|
573
623
|
|
|
574
624
|
def set_jira_base_url(self, value: str) -> None:
|
|
575
625
|
self.jira_base_url = value
|
|
626
|
+
self._drop_provider_results()
|
|
576
627
|
|
|
577
628
|
def set_jira_account_email(self, value: str) -> None:
|
|
578
629
|
self.jira_account_email = value
|
|
630
|
+
self._drop_provider_results()
|
|
579
631
|
|
|
580
632
|
def set_jira_api_token(self, value: str) -> None:
|
|
581
633
|
self.jira_api_token = value
|
|
634
|
+
self._drop_provider_results()
|
|
582
635
|
|
|
583
636
|
def set_jira_project(self, value: str) -> None:
|
|
584
637
|
self.jira_project = value
|
|
638
|
+
self._drop_provider_results()
|
|
585
639
|
|
|
586
640
|
def set_azure_organization_url(self, value: str) -> None:
|
|
587
641
|
self.azure_organization_url = value
|
|
642
|
+
self._drop_provider_results()
|
|
588
643
|
|
|
589
644
|
def set_azure_tenant_id(self, value: str) -> None:
|
|
590
645
|
self.azure_tenant_id = value
|
|
646
|
+
self._drop_provider_results()
|
|
591
647
|
|
|
592
648
|
def set_azure_client_id(self, value: str) -> None:
|
|
593
649
|
self.azure_client_id = value
|
|
650
|
+
self._drop_provider_results()
|
|
594
651
|
|
|
595
652
|
def set_azure_client_secret(self, value: str) -> None:
|
|
596
653
|
self.azure_client_secret = value
|
|
654
|
+
self._drop_provider_results()
|
|
597
655
|
|
|
598
656
|
@rx.var
|
|
599
657
|
def azure_can_connect(self) -> bool:
|
|
@@ -602,6 +660,101 @@ class AdminState(rx.State):
|
|
|
602
660
|
self.azure_organization_url,
|
|
603
661
|
self.azure_client_id, self.azure_client_secret))
|
|
604
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
|
+
|
|
605
758
|
def connect_azure_devops(self) -> Any:
|
|
606
759
|
"""Save the app registration, then hand the browser to Entra ID for consent.
|
|
607
760
|
|
|
@@ -622,35 +775,37 @@ class AdminState(rx.State):
|
|
|
622
775
|
|
|
623
776
|
def disconnect_azure_devops(self) -> Any:
|
|
624
777
|
SERVICE.disconnect_azure()
|
|
778
|
+
self._drop_provider_results()
|
|
625
779
|
self.load_azure_connection()
|
|
626
780
|
return rx.toast.success("Disconnected from Azure DevOps")
|
|
627
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
|
+
|
|
628
798
|
def _persist_settings(self) -> str:
|
|
629
799
|
"""Write the settings to disk. Returns an error message, or '' when saved."""
|
|
630
800
|
try:
|
|
631
801
|
parallel_agents = int(self.max_parallel_agents)
|
|
632
802
|
except ValueError:
|
|
633
803
|
return "Max parallel tasks must be a number"
|
|
634
|
-
credentials = (
|
|
635
|
-
{
|
|
636
|
-
"base_url": self.jira_base_url,
|
|
637
|
-
"account_email": self.jira_account_email,
|
|
638
|
-
"api_token": self.jira_api_token,
|
|
639
|
-
"project": self.jira_project,
|
|
640
|
-
}
|
|
641
|
-
if self.tasks_provider == "jira"
|
|
642
|
-
else {
|
|
643
|
-
"organization_url": self.azure_organization_url,
|
|
644
|
-
"tenant_id": self.azure_tenant_id,
|
|
645
|
-
"client_id": self.azure_client_id,
|
|
646
|
-
"client_secret": self.azure_client_secret,
|
|
647
|
-
}
|
|
648
|
-
)
|
|
649
804
|
SERVICE.save_settings(
|
|
650
805
|
self.tasks_provider,
|
|
651
806
|
self.coding_agent,
|
|
652
807
|
parallel_agents,
|
|
653
|
-
|
|
808
|
+
self._credentials(),
|
|
654
809
|
)
|
|
655
810
|
return ""
|
|
656
811
|
|
|
@@ -1627,6 +1782,105 @@ def azure_fields() -> rx.Component:
|
|
|
1627
1782
|
spacing="4", width="100%")
|
|
1628
1783
|
|
|
1629
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
|
+
|
|
1630
1884
|
def settings_page() -> rx.Component:
|
|
1631
1885
|
jira_fields = rx.vstack(
|
|
1632
1886
|
field("Base URL", rx.input(value=AdminState.jira_base_url,
|
|
@@ -1657,6 +1911,8 @@ def settings_page() -> rx.Component:
|
|
|
1657
1911
|
on_change=AdminState.set_tasks_provider, width="100%")),
|
|
1658
1912
|
rx.box(rx.cond(AdminState.tasks_provider == "jira",
|
|
1659
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"),
|
|
1660
1916
|
padding="1.25rem", background=SURFACE, border=BORDER, width="100%"),
|
|
1661
1917
|
rx.button(rx.icon("save", size=16), "Save settings",
|
|
1662
1918
|
on_click=AdminState.save_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
|