federaide-dev-nightly 2.0.1__tar.gz → 2.0.2__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.
- {federaide_dev_nightly-2.0.1/src/federaide_dev_nightly.egg-info → federaide_dev_nightly-2.0.2}/PKG-INFO +1 -1
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/pyproject.toml +1 -1
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2/src/federaide_dev_nightly.egg-info}/PKG-INFO +1 -1
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/agent.py +170 -51
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/toolbox.py +120 -61
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/LICENSE +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/MANIFEST.in +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/README.md +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/go/cmd/bridge/main.go +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/go/cmd/embed/main.go +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/go/cmd/search/main.go +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/go/go.mod +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/go/go.sum +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/scripts/build_go.sh +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/setup.cfg +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/setup.py +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federaide_dev_nightly.egg-info/SOURCES.txt +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federaide_dev_nightly.egg-info/dependency_links.txt +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federaide_dev_nightly.egg-info/entry_points.txt +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federaide_dev_nightly.egg-info/requires.txt +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federaide_dev_nightly.egg-info/top_level.txt +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/__init__.py +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/__main__.py +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/agent_core.py +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/agent_legacy.py +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/app.py +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/audio_handler.py +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/chatgpt_auth.py +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/commands.py +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/execution.py +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/mcp_handler.py +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/orchestration.py +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/semantic_search.py +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/styles/fonts/Michroma-Regular.ttf +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/styles/fonts/SpaceGrotesk-Bold.ttf +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/styles/fonts/SpaceGrotesk-VariableFont_wght.ttf +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/styles/style.css +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/subagents.py +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/telegram_handler.py +0 -0
- {federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/textIDE.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: federaide-dev-nightly
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: A cross-platform terminal based p2p agent orchestration harness and universal automation system
|
|
5
5
|
Author-email: ROCK-LAB-PRIVATE-LIMITED <arkadeep_ghosh@rocklab.in>
|
|
6
6
|
License: GNU Affero General Public License v3
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "federaide-dev-nightly"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.2"
|
|
8
8
|
description = "A cross-platform terminal based p2p agent orchestration harness and universal automation system"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "GNU Affero General Public License v3"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: federaide-dev-nightly
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: A cross-platform terminal based p2p agent orchestration harness and universal automation system
|
|
5
5
|
Author-email: ROCK-LAB-PRIVATE-LIMITED <arkadeep_ghosh@rocklab.in>
|
|
6
6
|
License: GNU Affero General Public License v3
|
|
@@ -294,6 +294,45 @@ class ClarificationModal(ModalScreen[str]):
|
|
|
294
294
|
def on_cancel(self):
|
|
295
295
|
self.dismiss("")
|
|
296
296
|
|
|
297
|
+
def get_git_commit_times(team_dir: str = None) -> dict:
|
|
298
|
+
times = {}
|
|
299
|
+
t_dir = team_dir or toolbox.get_team_dir()
|
|
300
|
+
try:
|
|
301
|
+
res = subprocess.run(
|
|
302
|
+
["git", "log", "--format=COMMIT:%ct", "--name-only", "--", "sessions", "nomem_sessions"],
|
|
303
|
+
cwd=t_dir,
|
|
304
|
+
capture_output=True,
|
|
305
|
+
text=True,
|
|
306
|
+
timeout=3
|
|
307
|
+
)
|
|
308
|
+
if res.returncode == 0:
|
|
309
|
+
cur_time = 0.0
|
|
310
|
+
for line in res.stdout.splitlines():
|
|
311
|
+
line = line.strip()
|
|
312
|
+
if line.startswith("COMMIT:"):
|
|
313
|
+
try: cur_time = float(line.split(":")[1])
|
|
314
|
+
except Exception: cur_time = 0.0
|
|
315
|
+
elif line and cur_time:
|
|
316
|
+
abs_p = os.path.normpath(os.path.join(t_dir, line))
|
|
317
|
+
if abs_p not in times:
|
|
318
|
+
times[abs_p] = cur_time
|
|
319
|
+
except Exception:
|
|
320
|
+
pass
|
|
321
|
+
return times
|
|
322
|
+
|
|
323
|
+
def get_session_file_time(fpath: str, git_times: dict = None) -> float:
|
|
324
|
+
norm = os.path.normpath(fpath)
|
|
325
|
+
if git_times and norm in git_times:
|
|
326
|
+
return git_times[norm]
|
|
327
|
+
parts = os.path.basename(fpath).replace(".json", "").split("_")
|
|
328
|
+
for p in parts:
|
|
329
|
+
if p.isdigit() and len(p) >= 9:
|
|
330
|
+
return float(p)
|
|
331
|
+
try:
|
|
332
|
+
return os.path.getmtime(fpath)
|
|
333
|
+
except Exception:
|
|
334
|
+
return 0.0
|
|
335
|
+
|
|
297
336
|
class ChatLoadModal(ModalScreen[str]):
|
|
298
337
|
DEFAULT_CSS = """
|
|
299
338
|
ChatLoadModal { align: center middle; background: $background 60%; }
|
|
@@ -310,7 +349,9 @@ class ChatLoadModal(ModalScreen[str]):
|
|
|
310
349
|
def compose(self) -> ComposeResult:
|
|
311
350
|
files_normal = glob.glob(toolbox.get_storage_path("sessions", "*.json"))
|
|
312
351
|
files_nomem = glob.glob(toolbox.get_storage_path("nomem_sessions", "*.json"))
|
|
313
|
-
|
|
352
|
+
raw_files = sorted(files_normal + files_nomem)
|
|
353
|
+
git_times = get_git_commit_times()
|
|
354
|
+
files = sorted(raw_files, key=lambda f: get_session_file_time(f, git_times), reverse=True)
|
|
314
355
|
name_map = agent_core.get_session_name_map()
|
|
315
356
|
|
|
316
357
|
def _extract_info(fpath):
|
|
@@ -1631,10 +1672,42 @@ class TeamDeleteModal(ModalScreen[str]):
|
|
|
1631
1672
|
else:
|
|
1632
1673
|
self.dismiss("cancel")
|
|
1633
1674
|
|
|
1675
|
+
class GitConflictModal(ModalScreen[str]):
|
|
1676
|
+
DEFAULT_CSS = """
|
|
1677
|
+
GitConflictModal { align: center middle; background: $background 60%; }
|
|
1678
|
+
#conflict_dialog { width: 70; height: auto; border: thick $error; background: $surface; padding: 1 2; }
|
|
1679
|
+
.conflict_msg { margin: 1 0; text-wrap: wrap; height: auto; }
|
|
1680
|
+
.conflict_buttons { layout: horizontal; height: auto; align: right middle; margin-top: 1; }
|
|
1681
|
+
.conflict_buttons Button { margin-left: 1; }
|
|
1682
|
+
"""
|
|
1683
|
+
def compose(self) -> ComposeResult:
|
|
1684
|
+
with Vertical(id="conflict_dialog"):
|
|
1685
|
+
yield Label(" Git History Conflict Detected!", classes="pane_title")
|
|
1686
|
+
yield Label(
|
|
1687
|
+
"The local and remote repository histories have diverged and cannot be automatically merged.\n\n"
|
|
1688
|
+
"Which branch would you like to keep?\n"
|
|
1689
|
+
"• Keep Local: Overwrites remote with your local files (Force Push).\n"
|
|
1690
|
+
"• Keep Remote: Overwrites local files with the remote repository.",
|
|
1691
|
+
classes="conflict_msg"
|
|
1692
|
+
)
|
|
1693
|
+
with Horizontal(classes="conflict_buttons"):
|
|
1694
|
+
yield Button("Keep Local (Force Push)", id="btn_keep_local", variant="success")
|
|
1695
|
+
yield Button("Keep Remote (Reset)", id="btn_keep_remote", variant="warning")
|
|
1696
|
+
yield Button("Cancel", id="btn_abort", variant="error")
|
|
1697
|
+
|
|
1698
|
+
@on(Button.Pressed)
|
|
1699
|
+
def handle_press(self, event: Button.Pressed):
|
|
1700
|
+
if event.button.id == "btn_keep_local":
|
|
1701
|
+
self.dismiss("local")
|
|
1702
|
+
elif event.button.id == "btn_keep_remote":
|
|
1703
|
+
self.dismiss("remote")
|
|
1704
|
+
else:
|
|
1705
|
+
self.dismiss("abort")
|
|
1706
|
+
|
|
1634
1707
|
class TeamsModal(ModalScreen[None]):
|
|
1635
1708
|
DEFAULT_CSS = """
|
|
1636
1709
|
TeamsModal { align: center middle; background: $background 60%; }
|
|
1637
|
-
#teams_dialog { width:
|
|
1710
|
+
#teams_dialog { width: 88; height: 85%; border: thick $primary; background: $surface; padding: 1 2; }
|
|
1638
1711
|
#teams_main_scroll { height: 1fr; border: round $accent; padding: 1; background: $boost; margin-bottom: 1; }
|
|
1639
1712
|
#team_form { height: auto; margin-bottom: 1; }
|
|
1640
1713
|
#team_list { height: auto; }
|
|
@@ -1647,8 +1720,8 @@ class TeamsModal(ModalScreen[None]):
|
|
|
1647
1720
|
#team_folder_input { width: 1fr; }
|
|
1648
1721
|
#team_git_user_input { width: 1fr; margin-right: 1; }
|
|
1649
1722
|
#team_git_pat_input { width: 1fr; }
|
|
1650
|
-
.
|
|
1651
|
-
.
|
|
1723
|
+
.team_form_row { layout: horizontal; height: auto; margin-top: 1; width: 100%; }
|
|
1724
|
+
.team_form_row Button { width: 1fr; margin: 0 1; }
|
|
1652
1725
|
#teams_bottom_buttons { height: auto; align: right middle; margin-top: 0; }
|
|
1653
1726
|
#teams_bottom_buttons Button { margin-left: 1; }
|
|
1654
1727
|
"""
|
|
@@ -1674,11 +1747,15 @@ class TeamsModal(ModalScreen[None]):
|
|
|
1674
1747
|
yield Input(placeholder="Git Username", id="team_git_user_input")
|
|
1675
1748
|
yield Input(placeholder="Personal Access Token (PAT)", id="team_git_pat_input", password=True)
|
|
1676
1749
|
|
|
1677
|
-
with Horizontal(classes="
|
|
1750
|
+
with Horizontal(classes="team_form_row"):
|
|
1678
1751
|
yield Button("Save Team", id="save_team_btn", variant="success")
|
|
1679
1752
|
yield Button("Restore", id="restore_btn", variant="primary")
|
|
1680
|
-
yield Button("Sync Now", id="sync_now_btn", variant="warning")
|
|
1681
1753
|
yield Button("Clear Form", id="clear_form_btn", variant="default")
|
|
1754
|
+
|
|
1755
|
+
with Horizontal(classes="team_form_row"):
|
|
1756
|
+
yield Button("Pull", id="pull_btn", variant="warning")
|
|
1757
|
+
yield Button("Push", id="push_btn", variant="warning")
|
|
1758
|
+
yield Button("Sync", id="sync_now_btn", variant="warning")
|
|
1682
1759
|
|
|
1683
1760
|
yield Label("Registered Teams:", classes="section_label")
|
|
1684
1761
|
yield Vertical(id="team_list")
|
|
@@ -1715,7 +1792,58 @@ class TeamsModal(ModalScreen[None]):
|
|
|
1715
1792
|
self.query_one("#team_remote_url_input", Input).value = ""
|
|
1716
1793
|
self.query_one("#team_git_user_input", Input).value = ""
|
|
1717
1794
|
self.query_one("#team_git_pat_input", Input).value = ""
|
|
1718
|
-
self.query_one("#save_team_btn", Button).label = "
|
|
1795
|
+
self.query_one("#save_team_btn", Button).label = "Save Team"
|
|
1796
|
+
|
|
1797
|
+
def _handle_git_action(self, action_name: str, target_team_dir: str):
|
|
1798
|
+
def _run_action():
|
|
1799
|
+
try:
|
|
1800
|
+
|
|
1801
|
+
ok, msg = toolbox.sync_team_remote(action=action_name, team_dir=target_team_dir)
|
|
1802
|
+
if ok:
|
|
1803
|
+
self.app.call_from_thread(self.notify, f"Git {action_name} completed successfully!", severity="information")
|
|
1804
|
+
if action_name in ["pull", "sync", "hard-reset-remote"]:
|
|
1805
|
+
self.agent_view.agent_manager.load_agents()
|
|
1806
|
+
self.agent_view.session_manager.sync_all_sessions()
|
|
1807
|
+
self.agent_view.update_status_bar()
|
|
1808
|
+
self.app.call_from_thread(self.refresh_list)
|
|
1809
|
+
elif msg == "CONFLICT":
|
|
1810
|
+
def _on_conflict():
|
|
1811
|
+
def _resolve(choice):
|
|
1812
|
+
if choice == "local":
|
|
1813
|
+
self.notify("Overwriting remote with local state (force push)...", severity="warning")
|
|
1814
|
+
self._handle_git_action("force-push", target_team_dir)
|
|
1815
|
+
elif choice == "remote":
|
|
1816
|
+
self.notify("Overwriting local state with remote...", severity="warning")
|
|
1817
|
+
self._handle_git_action("hard-reset-remote", target_team_dir)
|
|
1818
|
+
else:
|
|
1819
|
+
self.notify("Git operation cancelled by user.", severity="information")
|
|
1820
|
+
_reenable()
|
|
1821
|
+
self.app.push_screen(GitConflictModal(), _resolve)
|
|
1822
|
+
self.app.call_from_thread(_on_conflict)
|
|
1823
|
+
return
|
|
1824
|
+
else:
|
|
1825
|
+
self.app.call_from_thread(self.notify, f"Git {action_name} Failed: {msg}", severity="error", timeout=10.0)
|
|
1826
|
+
except Exception as e:
|
|
1827
|
+
self.app.call_from_thread(self.notify, f"Git Error: {e}", severity="error", timeout=10.0)
|
|
1828
|
+
finally:
|
|
1829
|
+
if msg != "CONFLICT":
|
|
1830
|
+
self.app.call_from_thread(_reenable)
|
|
1831
|
+
|
|
1832
|
+
def _reenable():
|
|
1833
|
+
try:
|
|
1834
|
+
self.query_one("#sync_now_btn", Button).disabled = False
|
|
1835
|
+
self.query_one("#pull_btn", Button).disabled = False
|
|
1836
|
+
self.query_one("#push_btn", Button).disabled = False
|
|
1837
|
+
except Exception: pass
|
|
1838
|
+
|
|
1839
|
+
try:
|
|
1840
|
+
self.query_one("#sync_now_btn", Button).disabled = True
|
|
1841
|
+
self.query_one("#pull_btn", Button).disabled = True
|
|
1842
|
+
self.query_one("#push_btn", Button).disabled = True
|
|
1843
|
+
except Exception: pass
|
|
1844
|
+
|
|
1845
|
+
import threading
|
|
1846
|
+
threading.Thread(target=_run_action, daemon=True).start()
|
|
1719
1847
|
|
|
1720
1848
|
def refresh_list(self):
|
|
1721
1849
|
container = self.query_one("#team_list")
|
|
@@ -1786,42 +1914,53 @@ class TeamsModal(ModalScreen[None]):
|
|
|
1786
1914
|
def _run_restore():
|
|
1787
1915
|
try:
|
|
1788
1916
|
from urllib.parse import urlparse, urlunparse, quote
|
|
1789
|
-
import subprocess
|
|
1917
|
+
import subprocess, tempfile, stat, contextlib, os
|
|
1790
1918
|
|
|
1919
|
+
@contextlib.contextmanager
|
|
1920
|
+
def git_askpass_context(pat_str: str):
|
|
1921
|
+
askpass_ext = ".bat" if os.name == "nt" else ".sh"
|
|
1922
|
+
fd, askpass_path = tempfile.mkstemp(suffix=askpass_ext, text=True)
|
|
1923
|
+
with os.fdopen(fd, "w") as f:
|
|
1924
|
+
if os.name == "nt":
|
|
1925
|
+
f.write("@echo off\nif \"%~1\"==\"\" exit /b 0\necho %FEDERAIDE_GIT_PASSWORD%\n")
|
|
1926
|
+
else:
|
|
1927
|
+
f.write("#!/bin/sh\necho \"$FEDERAIDE_GIT_PASSWORD\"\n")
|
|
1928
|
+
if os.name != "nt":
|
|
1929
|
+
os.chmod(askpass_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
|
|
1930
|
+
|
|
1931
|
+
env = os.environ.copy()
|
|
1932
|
+
env["GIT_ASKPASS"] = askpass_path
|
|
1933
|
+
env["FEDERAIDE_GIT_PASSWORD"] = pat_str
|
|
1934
|
+
env["GIT_TERMINAL_PROMPT"] = "0"
|
|
1935
|
+
try:
|
|
1936
|
+
yield env
|
|
1937
|
+
finally:
|
|
1938
|
+
try: os.remove(askpass_path)
|
|
1939
|
+
except Exception: pass
|
|
1940
|
+
|
|
1791
1941
|
parsed = urlparse(url)
|
|
1792
1942
|
user_part = quote(user, safe='') if user else "git"
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
authed_url = urlunparse((parsed.scheme, auth_netloc, parsed.path, parsed.params, parsed.query, parsed.fragment))
|
|
1943
|
+
auth_netloc = f"{user_part}@{parsed.netloc}"
|
|
1944
|
+
safe_url = urlunparse((parsed.scheme, auth_netloc, parsed.path, parsed.params, parsed.query, parsed.fragment))
|
|
1796
1945
|
|
|
1797
|
-
|
|
1946
|
+
with git_askpass_context(pat) as env:
|
|
1947
|
+
res = subprocess.run(["git", "clone", safe_url, target_path], capture_output=True, text=True, env=env)
|
|
1798
1948
|
|
|
1799
1949
|
if res.returncode == 0:
|
|
1800
|
-
# SECURITY FIX: Remove PAT from .git/config immediately after cloning
|
|
1801
1950
|
subprocess.run(["git", "remote", "set-url", "origin", url], cwd=target_path, capture_output=True)
|
|
1802
1951
|
def _on_success():
|
|
1803
1952
|
self.notify("Restore completed successfully!", severity="information")
|
|
1804
1953
|
t_name = self.query_one("#team_name_input", Input).value.strip() or os.path.basename(target_path)
|
|
1805
1954
|
self.dismiss()
|
|
1806
1955
|
|
|
1807
|
-
# Switch to the newly cloned workspace
|
|
1808
1956
|
self.agent_view.switch_team(target_path, display_name=t_name)
|
|
1809
|
-
|
|
1810
|
-
# Save credentials securely into the keyring
|
|
1811
|
-
import toolbox
|
|
1812
1957
|
toolbox.save_team_git_credentials(url, user, pat, team_dir=target_path)
|
|
1813
1958
|
|
|
1814
|
-
# Rebuild episodic and tool results databases in the background
|
|
1815
|
-
import threading
|
|
1816
1959
|
threading.Thread(target=self.agent_view.session_manager.sync_all_sessions, daemon=True).start()
|
|
1817
1960
|
|
|
1818
1961
|
self.app.call_from_thread(_on_success)
|
|
1819
1962
|
else:
|
|
1820
|
-
|
|
1821
|
-
if not txt: return ""
|
|
1822
|
-
clean = txt.replace(pat, "******") if pat else txt
|
|
1823
|
-
return clean.strip()
|
|
1824
|
-
err = _sanitize(res.stderr) or _sanitize(res.stdout) or "Clone failed."
|
|
1963
|
+
err = res.stderr.strip() or res.stdout.strip() or "Clone failed."
|
|
1825
1964
|
self.app.call_from_thread(self.notify, f"Restore Failed: {err}", severity="error", timeout=10.0)
|
|
1826
1965
|
except Exception as e:
|
|
1827
1966
|
self.app.call_from_thread(self.notify, f"Restore Error: {e}", severity="error", timeout=10.0)
|
|
@@ -1836,39 +1975,18 @@ class TeamsModal(ModalScreen[None]):
|
|
|
1836
1975
|
import threading
|
|
1837
1976
|
threading.Thread(target=_run_restore, daemon=True).start()
|
|
1838
1977
|
|
|
1839
|
-
elif btn_id
|
|
1978
|
+
elif btn_id in ("sync_now_btn", "pull_btn", "push_btn"):
|
|
1840
1979
|
url = self.query_one("#team_remote_url_input", Input).value.strip()
|
|
1841
1980
|
user = self.query_one("#team_git_user_input", Input).value.strip()
|
|
1842
1981
|
pat = self.query_one("#team_git_pat_input", Input).value.strip()
|
|
1843
1982
|
target_team_dir = self.editing_path or toolbox.get_team_dir()
|
|
1844
1983
|
|
|
1845
1984
|
toolbox.save_team_git_credentials(url, user, pat, team_dir=target_team_dir)
|
|
1846
|
-
self.notify(f"Syncing with Git remote for '{os.path.basename(target_team_dir)}'...", severity="information")
|
|
1847
1985
|
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
try:
|
|
1853
|
-
ok, msg = toolbox.sync_team_remote(action="sync", team_dir=target_team_dir)
|
|
1854
|
-
if ok:
|
|
1855
|
-
self.app.call_from_thread(self.notify, "Git remote sync completed successfully!", severity="information")
|
|
1856
|
-
self.agent_view.agent_manager.load_agents()
|
|
1857
|
-
self.agent_view.session_manager.sync_all_sessions()
|
|
1858
|
-
self.agent_view.update_status_bar()
|
|
1859
|
-
else:
|
|
1860
|
-
self.app.call_from_thread(self.notify, f"Sync Failed: {msg}", severity="error", timeout=10.0)
|
|
1861
|
-
except Exception as e:
|
|
1862
|
-
self.app.call_from_thread(self.notify, f"Sync Error: {e}", severity="error", timeout=10.0)
|
|
1863
|
-
finally:
|
|
1864
|
-
def _reenable():
|
|
1865
|
-
try:
|
|
1866
|
-
self.query_one("#sync_now_btn", Button).disabled = False
|
|
1867
|
-
except Exception:
|
|
1868
|
-
pass
|
|
1869
|
-
self.app.call_from_thread(_reenable)
|
|
1870
|
-
|
|
1871
|
-
threading.Thread(target=_run_sync, daemon=True).start()
|
|
1986
|
+
action_map = {"sync_now_btn": "sync", "pull_btn": "pull", "push_btn": "push"}
|
|
1987
|
+
action = action_map[btn_id]
|
|
1988
|
+
self.notify(f"{action.title()}ing with Git remote for '{os.path.basename(target_team_dir)}'...", severity="information")
|
|
1989
|
+
self._handle_git_action(action, target_team_dir)
|
|
1872
1990
|
|
|
1873
1991
|
elif btn_id == "save_team_btn":
|
|
1874
1992
|
t_name = self.query_one("#team_name_input", Input).value.strip()
|
|
@@ -3457,9 +3575,10 @@ class AIAgentView(Vertical):
|
|
|
3457
3575
|
self.log_to_ui("[bold red]No past chat sessions found to resume.[/bold red]")
|
|
3458
3576
|
return
|
|
3459
3577
|
|
|
3578
|
+
git_times = get_git_commit_times()
|
|
3460
3579
|
sorted_sessions = sorted(
|
|
3461
3580
|
sessions_map.items(),
|
|
3462
|
-
key=lambda item: max(
|
|
3581
|
+
key=lambda item: max(get_session_file_time(f, git_times) for f in item[1]),
|
|
3463
3582
|
reverse=True
|
|
3464
3583
|
)
|
|
3465
3584
|
|
|
@@ -548,7 +548,7 @@ def save_team_git_credentials(remote_url: str, username: str, pat: str, team_dir
|
|
|
548
548
|
|
|
549
549
|
def sync_team_remote(action: str = "push", team_dir: str = None) -> tuple[bool, str]:
|
|
550
550
|
"""
|
|
551
|
-
Executes an
|
|
551
|
+
Executes an authenticated Git remote operation.
|
|
552
552
|
The PAT is dynamically read from Keyring and never written to disk or git configs.
|
|
553
553
|
Returns (success, message).
|
|
554
554
|
"""
|
|
@@ -564,82 +564,141 @@ def sync_team_remote(action: str = "push", team_dir: str = None) -> tuple[bool,
|
|
|
564
564
|
|
|
565
565
|
try:
|
|
566
566
|
from urllib.parse import urlparse, urlunparse, quote
|
|
567
|
+
import tempfile, stat, contextlib
|
|
568
|
+
|
|
569
|
+
# 1. Commit any uncommitted changes first so git operations don't fail with "unstaged changes"
|
|
570
|
+
try:
|
|
571
|
+
flush_sqlite_databases()
|
|
572
|
+
subprocess.run(["git", "add", "."], cwd=t_dir, capture_output=True)
|
|
573
|
+
status = subprocess.run(["git", "status", "--porcelain"], cwd=t_dir, capture_output=True, text=True)
|
|
574
|
+
if status.stdout.strip():
|
|
575
|
+
subprocess.run(
|
|
576
|
+
["git", "-c", "user.name=FEDERaiDE", "-c", "user.email=harness@local", "commit", "-m", "Auto-commit before remote sync"],
|
|
577
|
+
cwd=t_dir, capture_output=True
|
|
578
|
+
)
|
|
579
|
+
except Exception:
|
|
580
|
+
pass
|
|
581
|
+
|
|
567
582
|
parsed = urlparse(remote_url)
|
|
568
583
|
if not parsed.scheme or not parsed.netloc:
|
|
569
584
|
return False, f"Invalid remote URL format: {remote_url}"
|
|
570
585
|
|
|
571
586
|
user_part = quote(username.strip(), safe='') if username else "git"
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
authed_url = urlunparse((parsed.scheme, auth_netloc, parsed.path, parsed.params, parsed.query, parsed.fragment))
|
|
575
|
-
|
|
576
|
-
env = os.environ.copy()
|
|
577
|
-
env["GIT_TERMINAL_PROMPT"] = "0"
|
|
578
|
-
|
|
579
|
-
# Define _sanitize immediately so it is in scope everywhere below
|
|
580
|
-
def _sanitize(txt: str) -> str:
|
|
581
|
-
if not txt: return ""
|
|
582
|
-
clean = txt.replace(pat, "******")
|
|
583
|
-
if pat_part != pat:
|
|
584
|
-
clean = clean.replace(pat_part, "******")
|
|
585
|
-
return clean.strip()
|
|
587
|
+
auth_netloc = f"{user_part}@{parsed.netloc}"
|
|
588
|
+
safe_url = urlunparse((parsed.scheme, auth_netloc, parsed.path, parsed.params, parsed.query, parsed.fragment))
|
|
586
589
|
|
|
587
590
|
branch_res = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=t_dir, capture_output=True, text=True)
|
|
588
591
|
branch = branch_res.stdout.strip() if branch_res.returncode == 0 and branch_res.stdout.strip() else "main"
|
|
589
592
|
user_args = ["-c", "user.name=FEDERaiDE", "-c", "user.email=harness@local"]
|
|
590
593
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
594
|
+
@contextlib.contextmanager
|
|
595
|
+
def git_askpass_context(pat_str: str):
|
|
596
|
+
askpass_ext = ".bat" if os.name == "nt" else ".sh"
|
|
597
|
+
fd, askpass_path = tempfile.mkstemp(suffix=askpass_ext, text=True)
|
|
598
|
+
with os.fdopen(fd, "w") as f:
|
|
599
|
+
if os.name == "nt":
|
|
600
|
+
f.write("@echo off\nif \"%~1\"==\"\" exit /b 0\necho %FEDERAIDE_GIT_PASSWORD%\n")
|
|
601
|
+
else:
|
|
602
|
+
f.write("#!/bin/sh\necho \"$FEDERAIDE_GIT_PASSWORD\"\n")
|
|
603
|
+
if os.name != "nt":
|
|
604
|
+
os.chmod(askpass_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
|
|
605
|
+
|
|
606
|
+
env = os.environ.copy()
|
|
607
|
+
env["GIT_ASKPASS"] = askpass_path
|
|
608
|
+
env["FEDERAIDE_GIT_PASSWORD"] = pat_str
|
|
609
|
+
env["GIT_TERMINAL_PROMPT"] = "0"
|
|
610
|
+
try:
|
|
611
|
+
yield env
|
|
612
|
+
finally:
|
|
613
|
+
try: os.remove(askpass_path)
|
|
614
|
+
except Exception: pass
|
|
597
615
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
616
|
+
with git_askpass_context(pat) as env:
|
|
617
|
+
if action == "pull":
|
|
618
|
+
cmd = ["git"] + user_args + ["pull", "--rebase", "--autostash", safe_url, branch]
|
|
619
|
+
res = subprocess.run(cmd, cwd=t_dir, capture_output=True, text=True, env=env, timeout=120)
|
|
620
|
+
if res.returncode == 0:
|
|
621
|
+
return True, "Synchronized successfully."
|
|
622
|
+
|
|
623
|
+
err_check = (res.stderr + res.stdout).lower()
|
|
624
|
+
subprocess.run(["git", "rebase", "--abort"], cwd=t_dir, capture_output=True, env=env)
|
|
625
|
+
subprocess.run(["git", "merge", "--abort"], cwd=t_dir, capture_output=True, env=env)
|
|
626
|
+
|
|
627
|
+
conflict_indicators = [
|
|
628
|
+
"conflict", "divergent", "unstaged changes", "cannot pull with rebase",
|
|
629
|
+
"unrelated histories", "failed to merge", "could not apply", "patch failed",
|
|
630
|
+
"non-fast-forward"
|
|
631
|
+
]
|
|
632
|
+
if any(ind in err_check for ind in conflict_indicators):
|
|
633
|
+
return False, "CONFLICT"
|
|
634
|
+
|
|
635
|
+
return False, res.stderr.strip() or res.stdout.strip() or f"Git exited with code {res.returncode}"
|
|
636
|
+
|
|
637
|
+
elif action == "push":
|
|
638
|
+
cmd = ["git", "push", safe_url, f"HEAD:{branch}"]
|
|
639
|
+
res = subprocess.run(cmd, cwd=t_dir, capture_output=True, text=True, env=env, timeout=120)
|
|
640
|
+
if res.returncode == 0:
|
|
641
|
+
return True, "Pushed successfully."
|
|
642
|
+
|
|
643
|
+
err_check = (res.stderr + res.stdout).lower()
|
|
644
|
+
conflict_indicators = [
|
|
645
|
+
"non-fast-forward", "fetch first", "rejected", "behind", "conflict"
|
|
646
|
+
]
|
|
647
|
+
if any(ind in err_check for ind in conflict_indicators):
|
|
648
|
+
return False, "CONFLICT"
|
|
649
|
+
|
|
650
|
+
return False, res.stderr.strip() or res.stdout.strip() or f"Git exited with code {res.returncode}"
|
|
651
|
+
|
|
652
|
+
elif action == "force-push":
|
|
653
|
+
cmd = ["git", "push", "--force", safe_url, f"HEAD:{branch}"]
|
|
654
|
+
res = subprocess.run(cmd, cwd=t_dir, capture_output=True, text=True, env=env, timeout=120)
|
|
655
|
+
if res.returncode == 0:
|
|
656
|
+
return True, "Force pushed successfully."
|
|
657
|
+
return False, res.stderr.strip() or res.stdout.strip() or f"Git exited with code {res.returncode}"
|
|
658
|
+
|
|
659
|
+
elif action == "hard-reset-remote":
|
|
660
|
+
fetch_cmd = ["git", "fetch", safe_url, branch]
|
|
661
|
+
fetch_res = subprocess.run(fetch_cmd, cwd=t_dir, capture_output=True, text=True, env=env, timeout=120)
|
|
662
|
+
if fetch_res.returncode != 0:
|
|
663
|
+
return False, fetch_res.stderr.strip() or fetch_res.stdout.strip() or "Fetch failed."
|
|
664
|
+
|
|
665
|
+
reset_cmd = ["git", "reset", "--hard", "FETCH_HEAD"]
|
|
666
|
+
res = subprocess.run(reset_cmd, cwd=t_dir, capture_output=True, text=True, env=env, timeout=120)
|
|
667
|
+
if res.returncode == 0:
|
|
668
|
+
return True, "Reset to remote state successfully."
|
|
669
|
+
return False, res.stderr.strip() or res.stdout.strip() or f"Git exited with code {res.returncode}"
|
|
670
|
+
|
|
671
|
+
elif action == "sync":
|
|
672
|
+
# 1. Try pushing first. If remote is brand new/empty, or local is up-to-date, this succeeds immediately!
|
|
673
|
+
push_cmd = ["git", "push", safe_url, f"HEAD:{branch}"]
|
|
674
|
+
res = subprocess.run(push_cmd, cwd=t_dir, capture_output=True, text=True, env=env, timeout=120)
|
|
675
|
+
if res.returncode == 0:
|
|
676
|
+
return True, "Synchronized successfully."
|
|
604
677
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
return True, "Synchronized successfully."
|
|
611
|
-
|
|
612
|
-
err_check = (res.stderr + res.stdout).lower()
|
|
613
|
-
# If rejected because remote has existing commits (non-fast-forward / fetch first)
|
|
614
|
-
if "fetch first" in err_check or "rejected" in err_check or "non-fast-forward" in err_check:
|
|
615
|
-
# 2. Pull remote commits with rebase
|
|
616
|
-
pull_cmd = ["git"] + user_args + ["pull", "--rebase", "--autostash", authed_url, branch]
|
|
617
|
-
pull_res = subprocess.run(pull_cmd, cwd=t_dir, capture_output=True, text=True, env=env, timeout=120)
|
|
618
|
-
if pull_res.returncode != 0:
|
|
619
|
-
subprocess.run(["git", "rebase", "--abort"], cwd=t_dir, capture_output=True, env=env)
|
|
620
|
-
# Fallback for unrelated histories (e.g. GitHub repo created with an initial README)
|
|
621
|
-
pull_merge_cmd = ["git"] + user_args + [
|
|
622
|
-
"pull", "--no-rebase", "--allow-unrelated-histories", "-X", "theirs", authed_url, branch
|
|
623
|
-
]
|
|
624
|
-
pull_res = subprocess.run(pull_merge_cmd, cwd=t_dir, capture_output=True, text=True, env=env, timeout=120)
|
|
678
|
+
err_check = (res.stderr + res.stdout).lower()
|
|
679
|
+
if any(ind in err_check for ind in ["fetch first", "rejected", "non-fast-forward", "behind"]):
|
|
680
|
+
# 2. Pull remote commits with rebase
|
|
681
|
+
pull_cmd = ["git"] + user_args + ["pull", "--rebase", "--autostash", safe_url, branch]
|
|
682
|
+
pull_res = subprocess.run(pull_cmd, cwd=t_dir, capture_output=True, text=True, env=env, timeout=120)
|
|
625
683
|
if pull_res.returncode != 0:
|
|
626
|
-
|
|
684
|
+
subprocess.run(["git", "rebase", "--abort"], cwd=t_dir, capture_output=True, env=env)
|
|
685
|
+
return False, "CONFLICT"
|
|
627
686
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
687
|
+
# 3. Re-push now that remote is integrated
|
|
688
|
+
final_push = subprocess.run(push_cmd, cwd=t_dir, capture_output=True, text=True, env=env, timeout=120)
|
|
689
|
+
if final_push.returncode == 0:
|
|
690
|
+
return True, "Synchronized successfully."
|
|
691
|
+
|
|
692
|
+
final_err = (final_push.stderr + final_push.stdout).lower()
|
|
693
|
+
if any(ind in final_err for ind in ["fetch first", "rejected", "non-fast-forward", "behind"]):
|
|
694
|
+
return False, "CONFLICT"
|
|
695
|
+
|
|
696
|
+
return False, final_push.stderr.strip() or final_push.stdout.strip() or f"Git push failed with code {final_push.returncode}"
|
|
633
697
|
|
|
634
|
-
|
|
635
|
-
return False, _sanitize(res.stderr) or _sanitize(res.stdout) or f"Git exited with code {res.returncode}"
|
|
698
|
+
return False, res.stderr.strip() or res.stdout.strip() or f"Git exited with code {res.returncode}"
|
|
636
699
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
res = subprocess.run(cmd, cwd=t_dir, capture_output=True, text=True, env=env, timeout=120)
|
|
640
|
-
if res.returncode == 0:
|
|
641
|
-
return True, "Synchronized successfully."
|
|
642
|
-
return False, _sanitize(res.stderr) or _sanitize(res.stdout) or f"Git exited with code {res.returncode}"
|
|
700
|
+
else:
|
|
701
|
+
return False, f"Unknown action: {action}"
|
|
643
702
|
|
|
644
703
|
except subprocess.TimeoutExpired:
|
|
645
704
|
return False, "Connection timed out after 120s."
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{federaide_dev_nightly-2.0.1 → federaide_dev_nightly-2.0.2}/src/federate/telegram_handler.py
RENAMED
|
File without changes
|
|
File without changes
|