iflow-mcp-m507_ai-soc-agent 1.0.0__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 (85) hide show
  1. iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/METADATA +410 -0
  2. iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/RECORD +85 -0
  3. iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/WHEEL +5 -0
  4. iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/entry_points.txt +2 -0
  5. iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/licenses/LICENSE +21 -0
  6. iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/top_level.txt +1 -0
  7. src/__init__.py +8 -0
  8. src/ai_controller/README.md +139 -0
  9. src/ai_controller/__init__.py +12 -0
  10. src/ai_controller/agent_executor.py +596 -0
  11. src/ai_controller/cli/__init__.py +2 -0
  12. src/ai_controller/cli/main.py +243 -0
  13. src/ai_controller/session_manager.py +409 -0
  14. src/ai_controller/web/__init__.py +2 -0
  15. src/ai_controller/web/server.py +1181 -0
  16. src/ai_controller/web/static/css/README.md +102 -0
  17. src/api/__init__.py +13 -0
  18. src/api/case_management.py +271 -0
  19. src/api/edr.py +187 -0
  20. src/api/kb.py +136 -0
  21. src/api/siem.py +308 -0
  22. src/core/__init__.py +10 -0
  23. src/core/config.py +242 -0
  24. src/core/config_storage.py +684 -0
  25. src/core/dto.py +50 -0
  26. src/core/errors.py +36 -0
  27. src/core/logging.py +128 -0
  28. src/integrations/__init__.py +8 -0
  29. src/integrations/case_management/__init__.py +5 -0
  30. src/integrations/case_management/iris/__init__.py +11 -0
  31. src/integrations/case_management/iris/iris_client.py +885 -0
  32. src/integrations/case_management/iris/iris_http.py +274 -0
  33. src/integrations/case_management/iris/iris_mapper.py +263 -0
  34. src/integrations/case_management/iris/iris_models.py +128 -0
  35. src/integrations/case_management/thehive/__init__.py +8 -0
  36. src/integrations/case_management/thehive/thehive_client.py +193 -0
  37. src/integrations/case_management/thehive/thehive_http.py +147 -0
  38. src/integrations/case_management/thehive/thehive_mapper.py +190 -0
  39. src/integrations/case_management/thehive/thehive_models.py +125 -0
  40. src/integrations/cti/__init__.py +6 -0
  41. src/integrations/cti/local_tip/__init__.py +10 -0
  42. src/integrations/cti/local_tip/local_tip_client.py +90 -0
  43. src/integrations/cti/local_tip/local_tip_http.py +110 -0
  44. src/integrations/cti/opencti/__init__.py +10 -0
  45. src/integrations/cti/opencti/opencti_client.py +101 -0
  46. src/integrations/cti/opencti/opencti_http.py +418 -0
  47. src/integrations/edr/__init__.py +6 -0
  48. src/integrations/edr/elastic_defend/__init__.py +6 -0
  49. src/integrations/edr/elastic_defend/elastic_defend_client.py +351 -0
  50. src/integrations/edr/elastic_defend/elastic_defend_http.py +162 -0
  51. src/integrations/eng/__init__.py +10 -0
  52. src/integrations/eng/clickup/__init__.py +8 -0
  53. src/integrations/eng/clickup/clickup_client.py +513 -0
  54. src/integrations/eng/clickup/clickup_http.py +156 -0
  55. src/integrations/eng/github/__init__.py +8 -0
  56. src/integrations/eng/github/github_client.py +169 -0
  57. src/integrations/eng/github/github_http.py +158 -0
  58. src/integrations/eng/trello/__init__.py +8 -0
  59. src/integrations/eng/trello/trello_client.py +207 -0
  60. src/integrations/eng/trello/trello_http.py +162 -0
  61. src/integrations/kb/__init__.py +12 -0
  62. src/integrations/kb/fs_kb_client.py +313 -0
  63. src/integrations/siem/__init__.py +6 -0
  64. src/integrations/siem/elastic/__init__.py +6 -0
  65. src/integrations/siem/elastic/elastic_client.py +3319 -0
  66. src/integrations/siem/elastic/elastic_http.py +165 -0
  67. src/mcp/README.md +183 -0
  68. src/mcp/TOOLS.md +2827 -0
  69. src/mcp/__init__.py +13 -0
  70. src/mcp/__main__.py +18 -0
  71. src/mcp/agent_profiles.py +408 -0
  72. src/mcp/flow_agent_profiles.py +424 -0
  73. src/mcp/mcp_server.py +4086 -0
  74. src/mcp/rules_engine.py +487 -0
  75. src/mcp/runbook_manager.py +264 -0
  76. src/orchestrator/__init__.py +11 -0
  77. src/orchestrator/incident_workflow.py +244 -0
  78. src/orchestrator/tools_case.py +1085 -0
  79. src/orchestrator/tools_cti.py +359 -0
  80. src/orchestrator/tools_edr.py +315 -0
  81. src/orchestrator/tools_eng.py +378 -0
  82. src/orchestrator/tools_kb.py +156 -0
  83. src/orchestrator/tools_siem.py +1709 -0
  84. src/web/__init__.py +8 -0
  85. src/web/config_server.py +511 -0
@@ -0,0 +1,243 @@
1
+ """
2
+ CLI entry point for cursor-agent command.
3
+
4
+ This acts as a wrapper that:
5
+ - Passes commands directly to Cursor IDE's cursor-agent binary
6
+ - Handles --web flag to start the SamiGPT AI Controller web interface
7
+
8
+ Usage:
9
+ cursor-agent "your prompt" # Passes to Cursor IDE cursor-agent
10
+ cursor-agent --web # Start SamiGPT AI Controller web server
11
+ cursor-agent --help # Shows Cursor IDE help
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import asyncio
18
+ import json
19
+ import os
20
+ import shutil
21
+ import subprocess
22
+ import sys
23
+ from typing import Optional
24
+
25
+ from ..agent_executor import AgentExecutor
26
+ from ..session_manager import SessionManager, SessionType
27
+ from ...core.config_storage import load_config_from_file
28
+ from ...core.logging import configure_logging, get_logger
29
+
30
+ logger = get_logger("sami.ai_controller.cli")
31
+
32
+
33
+ async def execute_command(command_str: str, session_name: Optional[str] = None) -> dict:
34
+ """Execute a command and return the result."""
35
+ executor = AgentExecutor(load_config_from_file())
36
+
37
+ # Optionally create a session if session_name is provided
38
+ session = None
39
+ session_manager = None
40
+ if session_name:
41
+ session_manager = SessionManager()
42
+ session = session_manager.create_session(session_name, SessionType.MANUAL)
43
+
44
+ # Add entry
45
+ entry = session_manager.add_entry(session.id, command_str)
46
+
47
+ # Parse and execute command
48
+ command = executor.parse_command(command_str)
49
+ result = await executor.execute_command(command)
50
+
51
+ # Update session if exists
52
+ if session and session_manager:
53
+ session_manager.update_entry(
54
+ session.id,
55
+ entry.id,
56
+ result=result.to_dict() if result else None,
57
+ status=result.status if hasattr(result, 'status') else None
58
+ )
59
+
60
+ return {
61
+ "success": result.success if result else False,
62
+ "output": result.output if result else None,
63
+ "error": result.error if result else None,
64
+ "session_id": session.id if session else None
65
+ }
66
+
67
+
68
+ def print_result(result: dict):
69
+ """Print the result in a formatted way."""
70
+ if result["success"]:
71
+ print("✓ Command executed successfully")
72
+ if result["output"]:
73
+ print("\nOutput:")
74
+ print(json.dumps(result["output"], indent=2))
75
+ else:
76
+ print("✗ Command failed")
77
+ if result["error"]:
78
+ print(f"\nError: {result['error']}")
79
+
80
+ if result.get("session_id"):
81
+ print(f"\nSession ID: {result['session_id']}")
82
+
83
+
84
+ def find_cursor_agent_binary():
85
+ """Find the actual Cursor IDE cursor-agent binary."""
86
+ # Possible locations
87
+ possible_paths = [
88
+ "/usr/local/bin/cursor-agent",
89
+ "/usr/bin/cursor-agent",
90
+ os.path.expanduser("~/.local/bin/cursor-agent"),
91
+ "/opt/homebrew/bin/cursor-agent",
92
+ ]
93
+
94
+ # Also check in PATH (but avoid this script's location)
95
+ script_dir = os.path.dirname(os.path.abspath(__file__))
96
+ venv_bin = os.path.join(script_dir, "../../../venv/bin")
97
+
98
+ # Try to find it using shutil.which, but exclude venv/bin
99
+ env_path = os.environ.get("PATH", "")
100
+ # Remove venv/bin from PATH temporarily
101
+ paths = [p for p in env_path.split(os.pathsep) if venv_bin not in p]
102
+ env_path_clean = os.pathsep.join(paths)
103
+
104
+ # Try possible paths first
105
+ for path in possible_paths:
106
+ if os.path.exists(path) and os.access(path, os.X_OK):
107
+ return path
108
+
109
+ # Try which command with cleaned PATH
110
+ old_path = os.environ.get("PATH", "")
111
+ try:
112
+ os.environ["PATH"] = env_path_clean
113
+ which_result = shutil.which("cursor-agent")
114
+ if which_result:
115
+ return which_result
116
+ finally:
117
+ os.environ["PATH"] = old_path
118
+
119
+ # Last resort: try which with original PATH but check it's not our script
120
+ result = shutil.which("cursor-agent")
121
+ if result and "venv/bin/cursor-agent" not in result:
122
+ return result
123
+
124
+ return None
125
+
126
+
127
+ def pass_to_cursor_agent(args):
128
+ """Pass all arguments to the actual Cursor IDE cursor-agent binary."""
129
+ cursor_agent_bin = find_cursor_agent_binary()
130
+
131
+ if not cursor_agent_bin:
132
+ print("Error: Cursor IDE cursor-agent binary not found.", file=sys.stderr)
133
+ print("Please ensure Cursor IDE is installed and cursor-agent is in your PATH.", file=sys.stderr)
134
+ sys.exit(1)
135
+
136
+ # Build command arguments - pass everything except --web
137
+ cmd_args = [cursor_agent_bin]
138
+
139
+ # Add all sys.argv except script name, but exclude --web and related args
140
+ exclude_args = {"--web", "--port", "--host", "--storage-dir", "--session"}
141
+ skip_next = False
142
+
143
+ for arg in sys.argv[1:]:
144
+ if skip_next:
145
+ skip_next = False
146
+ continue
147
+
148
+ if arg in exclude_args:
149
+ skip_next = True # Skip the next arg (value)
150
+ continue
151
+
152
+ # Don't skip args that start with -- if they're not in exclude list
153
+ if not arg.startswith("--"):
154
+ skip_next = False
155
+
156
+ cmd_args.append(arg)
157
+
158
+ # Execute the actual cursor-agent binary
159
+ try:
160
+ os.execv(cursor_agent_bin, cmd_args)
161
+ except Exception as e:
162
+ print(f"Error executing cursor-agent: {e}", file=sys.stderr)
163
+ sys.exit(1)
164
+
165
+
166
+ def main():
167
+ """Main CLI entry point."""
168
+ # Check for --web flag first (before any other parsing)
169
+ if "--web" in sys.argv:
170
+ # Handle web server mode
171
+ parser = argparse.ArgumentParser(
172
+ description="SamiGPT AI Controller Web Server",
173
+ formatter_class=argparse.RawDescriptionHelpFormatter,
174
+ )
175
+
176
+ parser.add_argument(
177
+ "--web",
178
+ action="store_true",
179
+ help="Start the web server interface"
180
+ )
181
+
182
+ parser.add_argument(
183
+ "--port",
184
+ type=int,
185
+ default=None,
186
+ help="Port for web server (default: from config or 8081)"
187
+ )
188
+
189
+ parser.add_argument(
190
+ "--host",
191
+ type=str,
192
+ default=None,
193
+ help="Host for web server (default: from config or 0.0.0.0)"
194
+ )
195
+
196
+ parser.add_argument(
197
+ "--storage-dir",
198
+ type=str,
199
+ help="Storage directory for sessions (default: data/ai_controller)"
200
+ )
201
+
202
+ parser.add_argument(
203
+ "--debug",
204
+ action="store_true",
205
+ help="Enable UI debug mode (show full JSON results in web UI)"
206
+ )
207
+
208
+ args = parser.parse_args()
209
+
210
+ # Configure logging
211
+ config = load_config_from_file()
212
+ configure_logging(config.logging if config.logging else None)
213
+
214
+ # Start web server
215
+ import uvicorn
216
+ from ..web.server import app, initialize
217
+
218
+ # Get config settings for web server from config.json
219
+ from ...core.config_storage import get_config_dict
220
+
221
+ config_dict = get_config_dict()
222
+ ai_controller_config = config_dict.get("ai_controller", {})
223
+
224
+ web_port = args.port or ai_controller_config.get("web_port", 8081)
225
+ web_host = args.host or ai_controller_config.get("web_host", "0.0.0.0")
226
+ storage_dir = args.storage_dir or ai_controller_config.get("storage_dir", "data/ai_controller")
227
+
228
+ # Initialize server
229
+ initialize(config_storage_dir=storage_dir, debug_ui=args.debug)
230
+
231
+ print(f"Starting SamiGPT AI Controller on http://{web_host}:{web_port}")
232
+ print("Press Ctrl+C to stop")
233
+
234
+ uvicorn.run(app, host=web_host, port=web_port, log_level="info")
235
+ return
236
+
237
+ # Not --web mode: pass through to Cursor IDE cursor-agent
238
+ pass_to_cursor_agent(None)
239
+
240
+
241
+ if __name__ == "__main__":
242
+ main()
243
+
@@ -0,0 +1,409 @@
1
+ """
2
+ Session manager for storing and managing agent execution sessions.
3
+
4
+ Stores sessions and autoruns in JSON files on the filesystem.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ from dataclasses import dataclass, asdict, field
12
+ from datetime import datetime
13
+ from enum import Enum
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional
16
+ from uuid import uuid4
17
+
18
+ from ..core.logging import get_logger
19
+
20
+ logger = get_logger("sami.ai_controller.session_manager")
21
+
22
+
23
+ class SessionType(Enum):
24
+ """Types of sessions."""
25
+ MANUAL = "manual" # User-initiated session
26
+ AUTORUN = "autorun" # Scheduled/recurring session
27
+
28
+
29
+ class SessionStatus(Enum):
30
+ """Status of a session."""
31
+ RUNNING = "running"
32
+ COMPLETED = "completed"
33
+ FAILED = "failed"
34
+ STOPPED = "stopped"
35
+ PENDING = "pending"
36
+
37
+
38
+ @dataclass
39
+ class SessionEntry:
40
+ """Represents a single execution entry in a session."""
41
+ id: str
42
+ command: str
43
+ timestamp: datetime
44
+ result: Optional[Dict[str, Any]] = None
45
+ status: SessionStatus = SessionStatus.PENDING
46
+
47
+ def to_dict(self) -> Dict[str, Any]:
48
+ """Convert to dictionary."""
49
+ data = asdict(self)
50
+ data["timestamp"] = self.timestamp.isoformat()
51
+ data["status"] = self.status.value
52
+ return data
53
+
54
+ @classmethod
55
+ def from_dict(cls, data: Dict[str, Any]) -> SessionEntry:
56
+ """Create from dictionary."""
57
+ data = data.copy()
58
+ data["timestamp"] = datetime.fromisoformat(data["timestamp"])
59
+ data["status"] = SessionStatus(data["status"])
60
+ return cls(**data)
61
+
62
+
63
+ @dataclass
64
+ class Session:
65
+ """Represents an agent execution session."""
66
+ id: str
67
+ name: str
68
+ session_type: SessionType
69
+ status: SessionStatus
70
+ created_at: datetime
71
+ updated_at: datetime
72
+ entries: List[SessionEntry] = field(default_factory=list)
73
+ autorun_config: Optional[Dict[str, Any]] = None # For autorun sessions
74
+
75
+ def to_dict(self) -> Dict[str, Any]:
76
+ """Convert to dictionary."""
77
+ data = asdict(self)
78
+ data["session_type"] = self.session_type.value
79
+ data["status"] = self.status.value
80
+ data["created_at"] = self.created_at.isoformat()
81
+ data["updated_at"] = self.updated_at.isoformat()
82
+ data["entries"] = [entry.to_dict() for entry in self.entries]
83
+ return data
84
+
85
+ @classmethod
86
+ def from_dict(cls, data: Dict[str, Any]) -> Session:
87
+ """Create from dictionary."""
88
+ data = data.copy()
89
+ data["session_type"] = SessionType(data["session_type"])
90
+ data["status"] = SessionStatus(data["status"])
91
+ data["created_at"] = datetime.fromisoformat(data["created_at"])
92
+ data["updated_at"] = datetime.fromisoformat(data["updated_at"])
93
+ data["entries"] = [SessionEntry.from_dict(entry) for entry in data.get("entries", [])]
94
+ return cls(**data)
95
+
96
+
97
+ @dataclass
98
+ class AutorunConfig:
99
+ """Configuration for an autorun session."""
100
+ id: str
101
+ name: str
102
+ command: str
103
+ interval_seconds: int
104
+ enabled: bool = True
105
+ session_id: Optional[str] = None # Persistent session used for this autorun
106
+ last_run: Optional[datetime] = None
107
+ next_run: Optional[datetime] = None
108
+ created_at: datetime = field(default_factory=datetime.now)
109
+ condition_function: Optional[str] = None # Function/tool name to check before executing (e.g., "get_recent_alerts")
110
+
111
+ def to_dict(self) -> Dict[str, Any]:
112
+ """Convert to dictionary."""
113
+ data = asdict(self)
114
+ if self.last_run:
115
+ data["last_run"] = self.last_run.isoformat()
116
+ if self.next_run:
117
+ data["next_run"] = self.next_run.isoformat()
118
+ data["created_at"] = self.created_at.isoformat()
119
+ return data
120
+
121
+ @classmethod
122
+ def from_dict(cls, data: Dict[str, Any]) -> AutorunConfig:
123
+ """Create from dictionary."""
124
+ data = data.copy()
125
+ if data.get("last_run"):
126
+ data["last_run"] = datetime.fromisoformat(data["last_run"])
127
+ if data.get("next_run"):
128
+ data["next_run"] = datetime.fromisoformat(data["next_run"])
129
+ if data.get("created_at"):
130
+ data["created_at"] = datetime.fromisoformat(data["created_at"])
131
+ return cls(**data)
132
+
133
+
134
+ class SessionManager:
135
+ """Manages agent execution sessions and autoruns."""
136
+
137
+ def __init__(self, storage_dir: str = "data/ai_controller"):
138
+ """Initialize the session manager."""
139
+ self.storage_dir = Path(storage_dir)
140
+ self.sessions_dir = self.storage_dir / "sessions"
141
+ self.autoruns_dir = self.storage_dir / "autoruns"
142
+
143
+ # Create directories if they don't exist
144
+ self.sessions_dir.mkdir(parents=True, exist_ok=True)
145
+ self.autoruns_dir.mkdir(parents=True, exist_ok=True)
146
+
147
+ # In-memory cache
148
+ self._sessions: Dict[str, Session] = {}
149
+ self._autoruns: Dict[str, AutorunConfig] = {}
150
+ self._load_all()
151
+
152
+ def _load_all(self):
153
+ """Load all sessions and autoruns from disk."""
154
+ # Load sessions
155
+ for session_file in self.sessions_dir.glob("*.json"):
156
+ try:
157
+ with open(session_file, "r") as f:
158
+ data = json.load(f)
159
+ session = Session.from_dict(data)
160
+ self._sessions[session.id] = session
161
+ except Exception as e:
162
+ logger.error(f"Failed to load session from {session_file}: {e}")
163
+
164
+ # Load autoruns
165
+ for autorun_file in self.autoruns_dir.glob("*.json"):
166
+ try:
167
+ with open(autorun_file, "r") as f:
168
+ data = json.load(f)
169
+ autorun = AutorunConfig.from_dict(data)
170
+ self._autoruns[autorun.id] = autorun
171
+ except Exception as e:
172
+ logger.error(f"Failed to load autorun from {autorun_file}: {e}")
173
+
174
+ logger.info(f"Loaded {len(self._sessions)} sessions and {len(self._autoruns)} autoruns")
175
+
176
+ def create_session(self, name: str, session_type: SessionType = SessionType.MANUAL) -> Session:
177
+ """Create a new session."""
178
+ session = Session(
179
+ id=str(uuid4()),
180
+ name=name,
181
+ session_type=session_type,
182
+ status=SessionStatus.PENDING,
183
+ created_at=datetime.now(),
184
+ updated_at=datetime.now()
185
+ )
186
+
187
+ self._sessions[session.id] = session
188
+ self._save_session(session)
189
+
190
+ logger.info("Created session: %s (%s, type=%s)", session.id, session.name, session.session_type.value)
191
+ return session
192
+
193
+ def get_session(self, session_id: str) -> Optional[Session]:
194
+ """Get a session by ID."""
195
+ return self._sessions.get(session_id)
196
+
197
+ def list_sessions(self, session_type: Optional[SessionType] = None) -> List[Session]:
198
+ """List all sessions, optionally filtered by type."""
199
+ sessions = list(self._sessions.values())
200
+ if session_type:
201
+ sessions = [s for s in sessions if s.session_type == session_type]
202
+ return sorted(sessions, key=lambda s: s.updated_at, reverse=True)
203
+
204
+ def add_entry(self, session_id: str, command: str, result: Optional[Dict[str, Any]] = None) -> SessionEntry:
205
+ """Add an entry to a session."""
206
+ session = self.get_session(session_id)
207
+ if not session:
208
+ raise ValueError(f"Session {session_id} not found")
209
+
210
+ entry = SessionEntry(
211
+ id=str(uuid4()),
212
+ command=command,
213
+ timestamp=datetime.now(),
214
+ result=result,
215
+ status=SessionStatus.COMPLETED if result else SessionStatus.PENDING
216
+ )
217
+
218
+ session.entries.append(entry)
219
+ session.updated_at = datetime.now()
220
+ session.status = SessionStatus.RUNNING if not result else SessionStatus.COMPLETED
221
+
222
+ logger.debug(
223
+ "Added entry %s to session %s (command=%s, immediate_status=%s)",
224
+ entry.id,
225
+ session_id,
226
+ command,
227
+ session.status.value,
228
+ )
229
+
230
+ self._save_session(session)
231
+
232
+ return entry
233
+
234
+ def update_entry(self, session_id: str, entry_id: str, result: Optional[Dict[str, Any]] = None, status: Optional[SessionStatus] = None):
235
+ """Update an entry in a session."""
236
+ session = self.get_session(session_id)
237
+ if not session:
238
+ raise ValueError(f"Session {session_id} not found")
239
+
240
+ entry = next((e for e in session.entries if e.id == entry_id), None)
241
+ if not entry:
242
+ raise ValueError(f"Entry {entry_id} not found in session {session_id}")
243
+
244
+ if result is not None:
245
+ entry.result = result
246
+ if status is not None:
247
+ entry.status = status
248
+
249
+ session.updated_at = datetime.now()
250
+ logger.debug(
251
+ "Updated entry %s in session %s (status=%s, has_result=%s)",
252
+ entry_id,
253
+ session_id,
254
+ entry.status.value,
255
+ result is not None,
256
+ )
257
+ self._save_session(session)
258
+
259
+ def update_session_status(self, session_id: str, status: SessionStatus):
260
+ """Update session status."""
261
+ session = self.get_session(session_id)
262
+ if not session:
263
+ raise ValueError(f"Session {session_id} not found")
264
+
265
+ session.status = status
266
+ session.updated_at = datetime.now()
267
+ logger.debug("Updated session %s status to %s", session_id, status.value)
268
+ self._save_session(session)
269
+
270
+ def delete_session(self, session_id: str):
271
+ """Delete a session."""
272
+ session = self.get_session(session_id)
273
+ if not session:
274
+ raise ValueError(f"Session {session_id} not found")
275
+
276
+ # Delete file
277
+ session_file = self.sessions_dir / f"{session_id}.json"
278
+ if session_file.exists():
279
+ try:
280
+ session_file.unlink()
281
+ logger.info(f"Deleted session file: {session_file}")
282
+ except Exception as e:
283
+ logger.error(f"Failed to delete session file {session_file}: {e}")
284
+ raise
285
+ else:
286
+ logger.warning(f"Session file does not exist: {session_file}")
287
+
288
+ # Remove from cache
289
+ if session_id in self._sessions:
290
+ del self._sessions[session_id]
291
+ logger.info(f"Removed session {session_id} from cache")
292
+ else:
293
+ logger.warning(f"Session {session_id} not in cache")
294
+
295
+ logger.info(f"Successfully deleted session: {session_id}")
296
+
297
+ def clear_session_entries(self, session_id: str):
298
+ """Clear all entries from a session."""
299
+ session = self.get_session(session_id)
300
+ if not session:
301
+ raise ValueError(f"Session {session_id} not found")
302
+
303
+ session.entries = []
304
+ session.updated_at = datetime.now()
305
+ logger.info(f"Cleared all entries from session: {session_id}")
306
+ self._save_session(session)
307
+
308
+ def _save_session(self, session: Session):
309
+ """Save a session to disk."""
310
+ session_file = self.sessions_dir / f"{session.id}.json"
311
+ with open(session_file, "w") as f:
312
+ json.dump(session.to_dict(), f, indent=2)
313
+
314
+ # Autorun methods
315
+ def create_autorun(self, name: str, command: str, interval_seconds: int, condition_function: Optional[str] = None) -> AutorunConfig:
316
+ """Create a new autorun configuration."""
317
+ # Create a dedicated AUTORUN session that will be reused for all executions
318
+ session = self.create_session(name, SessionType.AUTORUN)
319
+
320
+ now = datetime.now()
321
+ autorun = AutorunConfig(
322
+ id=str(uuid4()),
323
+ name=name,
324
+ command=command,
325
+ interval_seconds=interval_seconds,
326
+ enabled=True,
327
+ session_id=session.id,
328
+ created_at=now,
329
+ condition_function=condition_function,
330
+ next_run=now # Set to now so it runs immediately
331
+ )
332
+
333
+ self._autoruns[autorun.id] = autorun
334
+ self._save_autorun(autorun)
335
+
336
+ logger.info(f"Created autorun: {autorun.id} ({autorun.name}) - will run immediately, then every {interval_seconds}s")
337
+ return autorun
338
+
339
+ def get_autorun(self, autorun_id: str) -> Optional[AutorunConfig]:
340
+ """Get an autorun by ID."""
341
+ return self._autoruns.get(autorun_id)
342
+
343
+ def list_autoruns(self, enabled_only: bool = False) -> List[AutorunConfig]:
344
+ """List all autoruns."""
345
+ autoruns = list(self._autoruns.values())
346
+ if enabled_only:
347
+ autoruns = [a for a in autoruns if a.enabled]
348
+ return sorted(autoruns, key=lambda a: a.created_at, reverse=True)
349
+
350
+ def update_autorun(self, autorun_id: str, **kwargs):
351
+ """Update an autorun configuration."""
352
+ autorun = self.get_autorun(autorun_id)
353
+ if not autorun:
354
+ raise ValueError(f"Autorun {autorun_id} not found")
355
+
356
+ for key, value in kwargs.items():
357
+ if hasattr(autorun, key):
358
+ setattr(autorun, key, value)
359
+
360
+ self._save_autorun(autorun)
361
+
362
+ def delete_autorun(self, autorun_id: str):
363
+ """Delete an autorun and its dedicated session if present."""
364
+ autorun = self.get_autorun(autorun_id)
365
+ if not autorun:
366
+ raise ValueError(f"Autorun {autorun_id} not found")
367
+
368
+ logger.info(
369
+ "Deleting autorun config %s (%s) with session_id=%s",
370
+ autorun_id,
371
+ autorun.name,
372
+ autorun.session_id,
373
+ )
374
+
375
+ # Best-effort: also delete the associated AUTORUN session so it no longer appears in the UI
376
+ if autorun.session_id:
377
+ try:
378
+ logger.info("Deleting associated autorun session %s for autorun %s", autorun.session_id, autorun_id)
379
+ self.delete_session(autorun.session_id)
380
+ except Exception as e:
381
+ logger.error(
382
+ "Failed to delete associated autorun session %s for autorun %s: %s",
383
+ autorun.session_id,
384
+ autorun_id,
385
+ e,
386
+ )
387
+
388
+ # Delete autorun file
389
+ autorun_file = self.autoruns_dir / f"{autorun_id}.json"
390
+ if autorun_file.exists():
391
+ try:
392
+ autorun_file.unlink()
393
+ logger.info("Deleted autorun file: %s", autorun_file)
394
+ except Exception as e:
395
+ logger.error("Failed to delete autorun file %s: %s", autorun_file, e)
396
+ raise
397
+
398
+ # Remove from cache
399
+ if autorun_id in self._autoruns:
400
+ del self._autoruns[autorun_id]
401
+
402
+ logger.info("Successfully deleted autorun: %s", autorun_id)
403
+
404
+ def _save_autorun(self, autorun: AutorunConfig):
405
+ """Save an autorun to disk."""
406
+ autorun_file = self.autoruns_dir / f"{autorun.id}.json"
407
+ with open(autorun_file, "w") as f:
408
+ json.dump(autorun.to_dict(), f, indent=2)
409
+
@@ -0,0 +1,2 @@
1
+ """Web server for AI Controller."""
2
+