onecoder 0.0.2__py3-none-any.whl

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.
Files changed (73) hide show
  1. onecoder/agent.py +95 -0
  2. onecoder/agentic_tool_search/__init__.py +0 -0
  3. onecoder/agentic_tool_search/dynamic_tool_search.py +64 -0
  4. onecoder/agentic_tool_search/registry.py +33 -0
  5. onecoder/agents/__init__.py +7 -0
  6. onecoder/agents/documentation_agent.py +12 -0
  7. onecoder/agents/file_reader_agent.py +19 -0
  8. onecoder/agents/file_writer_agent.py +19 -0
  9. onecoder/agents/orchestrator_agent.py +51 -0
  10. onecoder/agents/refactoring_agent.py +12 -0
  11. onecoder/agents/research_agent.py +31 -0
  12. onecoder/agents/task_suggestion_agent.py +88 -0
  13. onecoder/alignment.py +236 -0
  14. onecoder/api.py +162 -0
  15. onecoder/api_client.py +112 -0
  16. onecoder/backends/base.py +22 -0
  17. onecoder/backends/local_tui.py +65 -0
  18. onecoder/blackboard.py +102 -0
  19. onecoder/cli.py +108 -0
  20. onecoder/commands/__init__.py +1 -0
  21. onecoder/commands/auth.py +78 -0
  22. onecoder/commands/ci.py +29 -0
  23. onecoder/commands/delegate.py +557 -0
  24. onecoder/commands/doctor.py +40 -0
  25. onecoder/commands/issue.py +136 -0
  26. onecoder/commands/logs.py +45 -0
  27. onecoder/commands/project.py +270 -0
  28. onecoder/commands/server.py +170 -0
  29. onecoder/config_manager.py +87 -0
  30. onecoder/constants.py +9 -0
  31. onecoder/diagnostics/__init__.py +2 -0
  32. onecoder/diagnostics/env_scan.py +207 -0
  33. onecoder/discovery.py +101 -0
  34. onecoder/distillation.py +236 -0
  35. onecoder/evaluation/__init__.py +1 -0
  36. onecoder/evaluation/ttu.py +176 -0
  37. onecoder/governance/__init__.py +0 -0
  38. onecoder/governance/probllm.py +91 -0
  39. onecoder/hooks.py +74 -0
  40. onecoder/ipc_auth.py +200 -0
  41. onecoder/issues.py +188 -0
  42. onecoder/jules_client.py +343 -0
  43. onecoder/knowledge.py +106 -0
  44. onecoder/llm.py +61 -0
  45. onecoder/logger.py +42 -0
  46. onecoder/metrics.py +129 -0
  47. onecoder/models/delegation.py +46 -0
  48. onecoder/onboarding.py +264 -0
  49. onecoder/review.py +233 -0
  50. onecoder/services/delegation_service.py +209 -0
  51. onecoder/services/validation_service.py +104 -0
  52. onecoder/sessions.py +186 -0
  53. onecoder/sprint_collector.py +165 -0
  54. onecoder/sync.py +167 -0
  55. onecoder/tmux.py +86 -0
  56. onecoder/tools/__init__.py +10 -0
  57. onecoder/tools/executor.py +53 -0
  58. onecoder/tools/external_tools.py +106 -0
  59. onecoder/tools/file_tools.py +77 -0
  60. onecoder/tools/interface.py +25 -0
  61. onecoder/tools/jules_tools.py +122 -0
  62. onecoder/tools/kit_tools.py +122 -0
  63. onecoder/tools/registry.py +32 -0
  64. onecoder/tui/__init__.py +5 -0
  65. onecoder/tui/app.py +263 -0
  66. onecoder/tui/commands.py +150 -0
  67. onecoder/tui/widgets.py +92 -0
  68. onecoder/worktree.py +186 -0
  69. onecoder-0.0.2.dist-info/METADATA +17 -0
  70. onecoder-0.0.2.dist-info/RECORD +73 -0
  71. onecoder-0.0.2.dist-info/WHEEL +5 -0
  72. onecoder-0.0.2.dist-info/entry_points.txt +2 -0
  73. onecoder-0.0.2.dist-info/top_level.txt +1 -0
onecoder/sessions.py ADDED
@@ -0,0 +1,186 @@
1
+ import os
2
+ import json
3
+ import time
4
+ import copy
5
+ import logging
6
+ from typing import Any, Optional, Dict, List
7
+ from pathlib import Path
8
+ from pydantic import BaseModel
9
+
10
+ from google.adk.sessions.base_session_service import BaseSessionService, GetSessionConfig, ListSessionsResponse
11
+ from google.adk.sessions.session import Session
12
+ from google.adk.events.event import Event
13
+ from google.adk.sessions.state import State
14
+ from google.adk.sessions import _session_util
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ class DurableSessionService(BaseSessionService):
19
+ """
20
+ A filesystem-backed session service for OneCoder.
21
+ Persists sessions, app state, and user state as JSON files.
22
+ """
23
+
24
+ def __init__(self, storage_dir: str = ".adk/sessions"):
25
+ self.storage_dir = Path(storage_dir)
26
+ self.sessions_dir = self.storage_dir / "sessions"
27
+ self.app_state_dir = self.storage_dir / "apps"
28
+ self.user_state_dir = self.storage_dir / "users"
29
+
30
+ # Ensure directories exist
31
+ self.sessions_dir.mkdir(parents=True, exist_ok=True)
32
+ self.app_state_dir.mkdir(parents=True, exist_ok=True)
33
+ self.user_state_dir.mkdir(parents=True, exist_ok=True)
34
+
35
+ def _get_session_path(self, app_name: str, user_id: str, session_id: str) -> Path:
36
+ return self.sessions_dir / app_name / user_id / f"{session_id}.json"
37
+
38
+ def _get_app_state_path(self, app_name: str) -> Path:
39
+ return self.app_state_dir / f"{app_name}.json"
40
+
41
+ def _get_user_state_path(self, app_name: str, user_id: str) -> Path:
42
+ return self.user_state_dir / app_name / f"{user_id}.json"
43
+
44
+ def _save_json(self, path: Path, data: Any):
45
+ path.parent.mkdir(parents=True, exist_ok=True)
46
+ with open(path, "w") as f:
47
+ if isinstance(data, BaseModel):
48
+ f.write(data.model_dump_json(by_alias=True, indent=2))
49
+ else:
50
+ json.dump(data, f, indent=2)
51
+
52
+ def _load_json(self, path: Path) -> Optional[Dict[str, Any]]:
53
+ if not path.exists():
54
+ return None
55
+ with open(path, "r") as f:
56
+ return json.load(f)
57
+
58
+ async def create_session(
59
+ self,
60
+ *,
61
+ app_name: str,
62
+ user_id: str,
63
+ state: Optional[dict[str, Any]] = None,
64
+ session_id: Optional[str] = None,
65
+ ) -> Session:
66
+ if not session_id:
67
+ session_id = f"session_{int(time.time())}"
68
+
69
+ session = Session(
70
+ id=session_id,
71
+ app_name=app_name,
72
+ user_id=user_id,
73
+ state=state or {},
74
+ last_update_time=time.time()
75
+ )
76
+
77
+ self._save_json(self._get_session_path(app_name, user_id, session_id), session)
78
+ return session
79
+
80
+ async def get_session(
81
+ self,
82
+ *,
83
+ app_name: str,
84
+ user_id: str,
85
+ session_id: str,
86
+ config: Optional[GetSessionConfig] = None,
87
+ ) -> Optional[Session]:
88
+ path = self._get_session_path(app_name, user_id, session_id)
89
+ data = self._load_json(path)
90
+ if not data:
91
+ return None
92
+
93
+ session = Session.model_validate(data)
94
+
95
+ # Filtering events based on config
96
+ if config:
97
+ if config.num_recent_events:
98
+ session.events = session.events[-config.num_recent_events:]
99
+ if config.after_timestamp:
100
+ session.events = [e for e in session.events if e.timestamp >= config.after_timestamp]
101
+
102
+ return self._merge_state(app_name, user_id, session)
103
+
104
+ def _merge_state(self, app_name: str, user_id: str, session: Session) -> Session:
105
+ # Load and merge app state
106
+ app_state = self._load_json(self._get_app_state_path(app_name)) or {}
107
+ for key, value in app_state.items():
108
+ session.state[State.APP_PREFIX + key] = value
109
+
110
+ # Load and merge user state
111
+ user_state = self._load_json(self._get_user_state_path(app_name, user_id)) or {}
112
+ for key, value in user_state.items():
113
+ session.state[State.USER_PREFIX + key] = value
114
+
115
+ return session
116
+
117
+ async def list_sessions(
118
+ self, *, app_name: str, user_id: Optional[str] = None
119
+ ) -> ListSessionsResponse:
120
+ sessions = []
121
+ app_path = self.sessions_dir / app_name
122
+ if not app_path.exists():
123
+ return ListSessionsResponse(sessions=[])
124
+
125
+ user_ids = [user_id] if user_id else [u.name for u in app_path.iterdir() if u.is_dir()]
126
+
127
+ for uid in user_ids:
128
+ user_path = app_path / uid
129
+ if not user_path.exists():
130
+ continue
131
+ for session_file in user_path.glob("*.json"):
132
+ data = self._load_json(session_file)
133
+ if data:
134
+ s = Session.model_validate(data)
135
+ s.events = [] # Following list_sessions contract (no events)
136
+ sessions.append(self._merge_state(app_name, uid, s))
137
+
138
+ return ListSessionsResponse(sessions=sessions)
139
+
140
+ async def delete_session(
141
+ self, *, app_name: str, user_id: str, session_id: str
142
+ ) -> None:
143
+ path = self._get_session_path(app_name, user_id, session_id)
144
+ if path.exists():
145
+ path.unlink()
146
+
147
+ async def append_event(self, session: Session, event: Event) -> Event:
148
+ if event.partial:
149
+ return event
150
+
151
+ # Update session object in memory (handles state delta too)
152
+ await super().append_event(session, event)
153
+ session.last_update_time = event.timestamp
154
+
155
+ # Load the stored session to apply events and state deltas
156
+ path = self._get_session_path(session.app_name, session.user_id, session.id)
157
+ data = self._load_json(path)
158
+ if not data:
159
+ logger.warning(f"Session file not found during append_event: {path}")
160
+ # If it's missing, we still save the passed session as a recovery
161
+ self._save_json(path, session)
162
+ return event
163
+
164
+ storage_session = Session.model_validate(data)
165
+ storage_session.events.append(event)
166
+ storage_session.last_update_time = event.timestamp
167
+
168
+ # Apply state deltas to persistent state files
169
+ if event.actions and event.actions.state_delta:
170
+ deltas = _session_util.extract_state_delta(event.actions.state_delta)
171
+
172
+ if deltas["app"]:
173
+ app_state = self._load_json(self._get_app_state_path(session.app_name)) or {}
174
+ app_state.update(deltas["app"])
175
+ self._save_json(self._get_app_state_path(session.app_name), app_state)
176
+
177
+ if deltas["user"]:
178
+ user_state = self._load_json(self._get_user_state_path(session.app_name, session.user_id)) or {}
179
+ user_state.update(deltas["user"])
180
+ self._save_json(self._get_user_state_path(session.app_name, session.user_id), user_state)
181
+
182
+ if deltas["session"]:
183
+ storage_session.state.update(deltas["session"])
184
+
185
+ self._save_json(path, storage_session)
186
+ return event
@@ -0,0 +1,165 @@
1
+ """
2
+ Sprint data collection module.
3
+
4
+ Scans .sprint/ directories and extracts sprint and task data for syncing to the API.
5
+ """
6
+
7
+ import re
8
+ import logging
9
+ from pathlib import Path
10
+ from typing import List, Dict, Any, Optional
11
+
12
+ try:
13
+ from ai_sprint.state import SprintStateManager
14
+ HAS_SPRINT_SDK = True
15
+ except ImportError:
16
+ HAS_SPRINT_SDK = False
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class SprintCollector:
22
+ """Collects sprint data from .sprint/ directories or via SDK."""
23
+
24
+ def __init__(self, repo_root: Path):
25
+ self.repo_root = repo_root
26
+ self.sprint_dir = repo_root / ".sprint"
27
+ self.state_manager = None
28
+ if HAS_SPRINT_SDK:
29
+ try:
30
+ self.state_manager = SprintStateManager(self.sprint_dir)
31
+ except Exception as e:
32
+ logger.warning(f"Could not initialize SprintStateManager: {e}")
33
+
34
+ def collect_all_sprints(self) -> List[Dict[str, Any]]:
35
+ """Scans .sprint/ and returns list of sprint data."""
36
+ sprints = []
37
+
38
+ if not self.sprint_dir.exists():
39
+ return sprints
40
+
41
+ for sprint_path in self.sprint_dir.iterdir():
42
+ if not sprint_path.is_dir():
43
+ continue
44
+
45
+ # Skip hidden directories
46
+ if sprint_path.name.startswith('.'):
47
+ continue
48
+
49
+ sprint_data = self._parse_sprint(sprint_path)
50
+ if sprint_data:
51
+ sprints.append(sprint_data)
52
+
53
+ return sprints
54
+
55
+ def _parse_sprint(self, sprint_path: Path) -> Optional[Dict[str, Any]]:
56
+ """Parses a single sprint directory."""
57
+ readme = sprint_path / "README.md"
58
+ todo = sprint_path / "TODO.md"
59
+ retro = sprint_path / "RETRO.md"
60
+
61
+ sprint_id = sprint_path.name
62
+
63
+ # Extract goal from README
64
+ goal = ""
65
+ title = sprint_id.replace("-", " ").title()
66
+
67
+ if readme.exists():
68
+ content = readme.read_text()
69
+
70
+ # Extract title from first heading
71
+ title_match = re.search(r'^#\s+Sprint:\s+(.+)$', content, re.MULTILINE)
72
+ if title_match:
73
+ title = title_match.group(1)
74
+
75
+ # Extract goal from ## Goal section
76
+ goal_match = re.search(r'##\s+Goal\s*\n+(.+?)(?:\n\n|\n##|$)', content, re.DOTALL)
77
+ if goal_match:
78
+ goal = goal_match.group(1).strip()
79
+
80
+ # Extract tasks from TODO.md
81
+ tasks = []
82
+ if todo.exists():
83
+ tasks = self._parse_todo(todo, sprint_id)
84
+
85
+ # Determine status
86
+ status = "closed" if retro.exists() else "active"
87
+
88
+ # Extract learnings from RETRO.md if available
89
+ learnings = []
90
+ if retro.exists():
91
+ learnings = self._parse_retro(retro)
92
+
93
+ return {
94
+ "id": sprint_id,
95
+ "title": title,
96
+ "status": status,
97
+ "goals": goal,
98
+ "tasks": tasks,
99
+ "learnings": learnings
100
+ }
101
+
102
+ def get_recent_context(self, limit: int = 3) -> List[Dict[str, Any]]:
103
+ """Returns context from the most recent sprints."""
104
+ all_sprints = self.collect_all_sprints()
105
+ # Sort by sprint ID (assuming lexicographical order roughly matches chronological or use metadata if available)
106
+ # Using simple string sort on ID for now, assuming format '000-name'
107
+ sorted_sprints = sorted(all_sprints, key=lambda x: x['id'], reverse=True)
108
+ return sorted_sprints[:limit]
109
+
110
+ def _parse_retro(self, retro_path: Path) -> List[str]:
111
+ """Mechanically extracts learnings from RETRO.md."""
112
+ learnings = []
113
+ content = retro_path.read_text()
114
+ capture = False
115
+ for line in content.split("\n"):
116
+ line_stripped = line.strip()
117
+ # Start capturing after these headers
118
+ if line_stripped.startswith("## ") and any(x in line_stripped for x in ["Learnings", "Learning", "Went Well", "To Improve", "Could Be Improved", "Action Items"]):
119
+ capture = True
120
+ continue
121
+ elif line_stripped.startswith("## "):
122
+ capture = False
123
+
124
+ if capture:
125
+ if line_stripped.startswith("- "):
126
+ learnings.append(line_stripped[2:].strip())
127
+ elif re.match(r'^\d+\.\s+', line_stripped):
128
+ learnings.append(re.sub(r'^\d+\.\s+', '', line_stripped).strip())
129
+ return learnings
130
+
131
+ def _parse_todo(self, todo_path: Path, sprint_id: str) -> List[Dict[str, Any]]:
132
+ """Parses TODO.md and extracts tasks."""
133
+ tasks = []
134
+ content = todo_path.read_text()
135
+
136
+ task_counter = 1
137
+ for line in content.split("\n"):
138
+ line_stripped = line.strip()
139
+
140
+ # Match task lines: - [ ], - [x], - [/]
141
+ if re.match(r'^-\s+\[.\]', line_stripped):
142
+ # Extract checkbox status
143
+ is_done = re.match(r'^-\s+\[x\]', line_stripped, re.IGNORECASE)
144
+ is_in_progress = re.match(r'^-\s+\[/\]', line_stripped)
145
+
146
+ # Extract task title (everything after the checkbox)
147
+ title_match = re.search(r'^-\s+\[.\]\s+(.+)$', line_stripped)
148
+ if not title_match:
149
+ continue
150
+
151
+ title = title_match.group(1).strip()
152
+
153
+ # Generate task ID
154
+ task_id = f"{sprint_id}-task-{task_counter:03d}"
155
+
156
+ tasks.append({
157
+ "id": task_id,
158
+ "title": title,
159
+ "status": "done" if is_done else ("in-progress" if is_in_progress else "todo"),
160
+ "metadata": {}
161
+ })
162
+
163
+ task_counter += 1
164
+
165
+ return tasks
onecoder/sync.py ADDED
@@ -0,0 +1,167 @@
1
+ import os
2
+ import json
3
+ import click
4
+ from pathlib import Path
5
+ from typing import Optional, Dict, Any
6
+ from .api_client import get_api_client
7
+ from .config_manager import config_manager
8
+ from .sprint_collector import SprintCollector
9
+ from .issues import IssueManager
10
+
11
+ class ProjectConfig:
12
+ def __init__(self, repo_root: Path):
13
+ self.repo_root = repo_root
14
+ self.config_file = repo_root / ".onecoder.json"
15
+
16
+ def load(self) -> Dict[str, Any]:
17
+ if not self.config_file.exists():
18
+ return {}
19
+ try:
20
+ with open(self.config_file, "r") as f:
21
+ return json.load(f)
22
+ except Exception as e:
23
+ click.echo(f"Error loading project config: {e}")
24
+ return {}
25
+
26
+ def save(self, config: Dict[str, Any]):
27
+ try:
28
+ with open(self.config_file, "w") as f:
29
+ json.dump(config, f, indent=4)
30
+ except Exception as e:
31
+ click.echo(f"Error saving project config: {e}")
32
+
33
+ def get_project_id(self) -> Optional[str]:
34
+ # Priority: Env Var > Local Config
35
+ project_id = os.getenv("ONECODER_PROJECT_ID")
36
+ if project_id:
37
+ return project_id
38
+ return self.load().get("project_id")
39
+
40
+ def find_repo_root() -> Path:
41
+ current = Path.cwd()
42
+ while current != current.parent:
43
+ if (current / ".git").exists():
44
+ return current
45
+ current = current.parent
46
+ return Path.cwd()
47
+
48
+ async def sync_sprint(client, project_id: str, sprint: Dict[str, Any]):
49
+ """Syncs a single sprint to the API."""
50
+ try:
51
+ payload = {
52
+ "sprint": {
53
+ "id": sprint["id"],
54
+ "title": sprint["title"],
55
+ "status": sprint["status"],
56
+ "goals": sprint["goals"]
57
+ },
58
+ "tasks": sprint["tasks"],
59
+ "projectId": project_id
60
+ }
61
+
62
+ result = await client.sync_sprint(payload)
63
+ click.secho(f" ✓ {sprint['id']}: {len(sprint['tasks'])} tasks", fg="green")
64
+ return result
65
+ except Exception as e:
66
+ click.secho(f" ✗ {sprint['id']}: {e}", fg="red")
67
+ return None
68
+
69
+ async def sync_project_context():
70
+ """Aggregates local context and syncs with the API."""
71
+ repo_root = find_repo_root()
72
+ proj_config = ProjectConfig(repo_root)
73
+ project_id = proj_config.get_project_id()
74
+
75
+ if not project_id:
76
+ click.secho("Error: No project ID found. Run 'onecoder login' or set ONECODER_PROJECT_ID.", fg="red")
77
+ return
78
+
79
+ token = config_manager.get_token()
80
+ if not token:
81
+ click.secho("Error: Not logged in. Run 'onecoder login'.", fg="red")
82
+ return
83
+
84
+ # Aggregate context
85
+ spec_path = repo_root / "SPECIFICATION.md"
86
+ gov_path = repo_root / "governance.yaml"
87
+ learnings_path = repo_root / "ANTIGRAVITY.md"
88
+
89
+ sync_data = {
90
+ "metadata": {}
91
+ }
92
+
93
+ if spec_path.exists():
94
+ sync_data["specification"] = spec_path.read_text()
95
+
96
+ if gov_path.exists():
97
+ sync_data["governance"] = gov_path.read_text()
98
+
99
+ if learnings_path.exists():
100
+ sync_data["metadata"]["learnings"] = learnings_path.read_text()
101
+
102
+ # Get project name from folder if not in config
103
+ if not sync_data.get("name"):
104
+ sync_data["name"] = repo_root.name
105
+
106
+ try:
107
+ client = get_api_client(token)
108
+
109
+ # Sync project metadata
110
+ result = await client.sync_project(project_id, sync_data)
111
+ click.secho(f"✅ Project context synced successfully for ID: {project_id}", fg="green")
112
+
113
+ # NEW: Sync sprint data
114
+ collector = SprintCollector(repo_root)
115
+ sprints = collector.collect_all_sprints()
116
+
117
+ if sprints:
118
+ click.secho(f"\n📦 Syncing {len(sprints)} sprints...", fg="cyan")
119
+ for sprint in sprints:
120
+ await sync_sprint(client, project_id, sprint)
121
+ click.secho(f"✅ All sprints synced successfully", fg="green")
122
+ else:
123
+ click.secho("\nℹ️ No sprints found to sync", fg="yellow")
124
+
125
+ return result
126
+ return result
127
+ except Exception as e:
128
+ click.secho(f"❌ Failed to sync project: {e}", fg="red")
129
+ return None
130
+
131
+ async def sync_issues(client, project_id: str):
132
+ """Syncs local issues to the API."""
133
+ manager = IssueManager()
134
+ issues = manager.get_all_issues()
135
+
136
+ if not issues:
137
+ click.secho("ℹ️ No issues found to sync", fg="yellow")
138
+ return
139
+
140
+ click.secho(f"📦 Syncing {len(issues)} issues...", fg="cyan")
141
+
142
+ try:
143
+ payload = {
144
+ "projectId": project_id,
145
+ "issues": issues
146
+ }
147
+
148
+ # We need to add sync_issues to api_client first?
149
+ # Or we can just use client.post directly if method not exists.
150
+ # Let's assume client has a generic method or we add specific one.
151
+ # Actually, let's look at api_client.py later. For now, assuming client.sync_issues exists or using _request.
152
+ # Wait, I should check api_client.py. But for now I'll use a generic call pattern if possible
153
+ # or assume I'll update api_client.py too.
154
+ # Let's just use the direct route for now if client exposes request.
155
+
156
+ # Checking previous code, client seems to have typed methods.
157
+ # I should probably update api_client.py as well.
158
+ # But let's write the call here assuming the method exists, and then go fix api_client.
159
+
160
+ result = await client.sync_issues(payload)
161
+
162
+ stats = result.get("stats", {})
163
+ click.secho(f"✅ Issues synced: {stats.get('upserted', 0)} upserted, {stats.get('errors', 0)} errors", fg="green")
164
+
165
+ except Exception as e:
166
+ click.secho(f"❌ Failed to sync issues: {e}", fg="red")
167
+
onecoder/tmux.py ADDED
@@ -0,0 +1,86 @@
1
+ import subprocess
2
+ import logging
3
+ from typing import List, Optional
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ class TmuxManager:
8
+ """
9
+ Manages tmux sessions and panes for delegated tasks.
10
+ """
11
+
12
+ def __init__(self):
13
+ self._check_tmux()
14
+
15
+ def _check_tmux(self):
16
+ try:
17
+ subprocess.run(["tmux", "-V"], capture_output=True, check=True)
18
+ except (subprocess.CalledProcessError, FileNotFoundError):
19
+ raise RuntimeError("tmux is not installed or not available in PATH")
20
+
21
+ def _run_tmux(self, args: List[str]) -> str:
22
+ command = ["tmux"] + args
23
+ result = subprocess.run(command, capture_output=True, text=True, check=False)
24
+ if result.returncode != 0:
25
+ logger.debug(f"Tmux command failed: {' '.join(command)}\nError: {result.stderr}")
26
+ raise RuntimeError(f"Tmux error: {result.stderr.strip()}")
27
+ return result.stdout.strip()
28
+
29
+ def has_session(self, name: str) -> bool:
30
+ try:
31
+ self._run_tmux(["has-session", "-t", name])
32
+ return True
33
+ except RuntimeError:
34
+ return False
35
+
36
+ def create_session(self, name: str, command: Optional[str] = None, cwd: Optional[str] = None) -> None:
37
+ """Creates a new detached tmux session."""
38
+ args = ["new-session", "-d", "-s", name]
39
+ if cwd:
40
+ args += ["-c", cwd]
41
+ if command:
42
+ args.append(command)
43
+ self._run_tmux(args)
44
+
45
+ def split_window(self, target_session: str, command: Optional[str] = None, horizontal: bool = True) -> None:
46
+ """Splits the current window of a session."""
47
+ args = ["split-window", "-t", target_session]
48
+ if horizontal:
49
+ args.append("-h")
50
+ else:
51
+ args.append("-v")
52
+ if command:
53
+ args.append(command)
54
+ self._run_tmux(args)
55
+
56
+ def kill_session(self, name: str) -> None:
57
+ """Kills a tmux session."""
58
+ if self.has_session(name):
59
+ self._run_tmux(["kill-session", "-t", name])
60
+
61
+ def list_sessions(self) -> List[str]:
62
+ """Lists all tmux sessions."""
63
+ try:
64
+ output = self._run_tmux(["list-sessions", "-F", "#S"])
65
+ return output.split("\n")
66
+ except RuntimeError:
67
+ return []
68
+
69
+ def send_keys(self, target: str, keys: str, enter: bool = True) -> None:
70
+ """Sends keys to a tmux session/pane."""
71
+ args = ["send-keys", "-t", target, keys]
72
+ if enter:
73
+ args.append("C-m")
74
+ self._run_tmux(args)
75
+
76
+ if __name__ == "__main__":
77
+ # Quick test
78
+ import time
79
+ mgr = TmuxManager()
80
+ session = "test-session"
81
+ print(f"Creating session: {session}")
82
+ mgr.create_session(session, "top")
83
+ time.sleep(2)
84
+ print(f"Sessions: {mgr.list_sessions()}")
85
+ mgr.kill_session(session)
86
+ print("Session killed.")
@@ -0,0 +1,10 @@
1
+ from .registry import registry
2
+ from .interface import BaseTool
3
+
4
+ # Dynamic Discovery
5
+ def _initialize_tools():
6
+ from ..discovery import DiscoveryAgent
7
+ disco = DiscoveryAgent("onecoder.tools")
8
+ disco.discover_and_register()
9
+
10
+ _initialize_tools()
@@ -0,0 +1,53 @@
1
+
2
+ import re
3
+ from typing import Any, Dict, Optional, List
4
+ from rank_bm25 import BM25Okapi
5
+ from .registry import registry
6
+
7
+ class DynamicToolExecutor:
8
+ """
9
+ Manages discovery and execution of tools using BM25 for ranking.
10
+ """
11
+ def __init__(self):
12
+ self.registry = registry
13
+
14
+ def _tokenize(self, text: str) -> List[str]:
15
+ return re.findall(r'\w+', text.lower())
16
+
17
+ async def find_and_execute_tool(self, query: str, **tool_args) -> Any:
18
+ """
19
+ Finds the best tool for the query using BM25 and executes it.
20
+ """
21
+ tools = self.registry.list_tools()
22
+ if not tools:
23
+ return "No tools registered in the system."
24
+
25
+ # Prepare corpus
26
+ tool_descriptions = [tool.description for tool in tools]
27
+ tokenized_corpus = [self._tokenize(doc) for doc in tool_descriptions]
28
+
29
+ # Init BM25
30
+ bm25 = BM25Okapi(tokenized_corpus)
31
+
32
+ # Query
33
+ tokenized_query = self._tokenize(query)
34
+ scores = bm25.get_scores(tokenized_query)
35
+
36
+ # Find best
37
+ best_index = scores.argmax()
38
+ best_score = scores[best_index]
39
+ best_tool = tools[best_index]
40
+
41
+ print(f"[DynamicToolExecutor] Selected tool: '{best_tool.name}' (Score: {best_score:.4f}) for query: '{query}'")
42
+
43
+ # Heuristic threshold could be added here, but for now we just take the top one
44
+ if best_score == 0:
45
+ print(f"[DynamicToolExecutor] Warning: Zero score match for query '{query}'. Defaulting to '{best_tool.name}'")
46
+
47
+ try:
48
+ return await best_tool.execute_async(**tool_args)
49
+ except Exception as e:
50
+ return f"Error executing tool '{best_tool.name}': {e}"
51
+
52
+ # Global executor instance
53
+ executor = DynamicToolExecutor()