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.
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/METADATA +410 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/RECORD +85 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/WHEEL +5 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/top_level.txt +1 -0
- src/__init__.py +8 -0
- src/ai_controller/README.md +139 -0
- src/ai_controller/__init__.py +12 -0
- src/ai_controller/agent_executor.py +596 -0
- src/ai_controller/cli/__init__.py +2 -0
- src/ai_controller/cli/main.py +243 -0
- src/ai_controller/session_manager.py +409 -0
- src/ai_controller/web/__init__.py +2 -0
- src/ai_controller/web/server.py +1181 -0
- src/ai_controller/web/static/css/README.md +102 -0
- src/api/__init__.py +13 -0
- src/api/case_management.py +271 -0
- src/api/edr.py +187 -0
- src/api/kb.py +136 -0
- src/api/siem.py +308 -0
- src/core/__init__.py +10 -0
- src/core/config.py +242 -0
- src/core/config_storage.py +684 -0
- src/core/dto.py +50 -0
- src/core/errors.py +36 -0
- src/core/logging.py +128 -0
- src/integrations/__init__.py +8 -0
- src/integrations/case_management/__init__.py +5 -0
- src/integrations/case_management/iris/__init__.py +11 -0
- src/integrations/case_management/iris/iris_client.py +885 -0
- src/integrations/case_management/iris/iris_http.py +274 -0
- src/integrations/case_management/iris/iris_mapper.py +263 -0
- src/integrations/case_management/iris/iris_models.py +128 -0
- src/integrations/case_management/thehive/__init__.py +8 -0
- src/integrations/case_management/thehive/thehive_client.py +193 -0
- src/integrations/case_management/thehive/thehive_http.py +147 -0
- src/integrations/case_management/thehive/thehive_mapper.py +190 -0
- src/integrations/case_management/thehive/thehive_models.py +125 -0
- src/integrations/cti/__init__.py +6 -0
- src/integrations/cti/local_tip/__init__.py +10 -0
- src/integrations/cti/local_tip/local_tip_client.py +90 -0
- src/integrations/cti/local_tip/local_tip_http.py +110 -0
- src/integrations/cti/opencti/__init__.py +10 -0
- src/integrations/cti/opencti/opencti_client.py +101 -0
- src/integrations/cti/opencti/opencti_http.py +418 -0
- src/integrations/edr/__init__.py +6 -0
- src/integrations/edr/elastic_defend/__init__.py +6 -0
- src/integrations/edr/elastic_defend/elastic_defend_client.py +351 -0
- src/integrations/edr/elastic_defend/elastic_defend_http.py +162 -0
- src/integrations/eng/__init__.py +10 -0
- src/integrations/eng/clickup/__init__.py +8 -0
- src/integrations/eng/clickup/clickup_client.py +513 -0
- src/integrations/eng/clickup/clickup_http.py +156 -0
- src/integrations/eng/github/__init__.py +8 -0
- src/integrations/eng/github/github_client.py +169 -0
- src/integrations/eng/github/github_http.py +158 -0
- src/integrations/eng/trello/__init__.py +8 -0
- src/integrations/eng/trello/trello_client.py +207 -0
- src/integrations/eng/trello/trello_http.py +162 -0
- src/integrations/kb/__init__.py +12 -0
- src/integrations/kb/fs_kb_client.py +313 -0
- src/integrations/siem/__init__.py +6 -0
- src/integrations/siem/elastic/__init__.py +6 -0
- src/integrations/siem/elastic/elastic_client.py +3319 -0
- src/integrations/siem/elastic/elastic_http.py +165 -0
- src/mcp/README.md +183 -0
- src/mcp/TOOLS.md +2827 -0
- src/mcp/__init__.py +13 -0
- src/mcp/__main__.py +18 -0
- src/mcp/agent_profiles.py +408 -0
- src/mcp/flow_agent_profiles.py +424 -0
- src/mcp/mcp_server.py +4086 -0
- src/mcp/rules_engine.py +487 -0
- src/mcp/runbook_manager.py +264 -0
- src/orchestrator/__init__.py +11 -0
- src/orchestrator/incident_workflow.py +244 -0
- src/orchestrator/tools_case.py +1085 -0
- src/orchestrator/tools_cti.py +359 -0
- src/orchestrator/tools_edr.py +315 -0
- src/orchestrator/tools_eng.py +378 -0
- src/orchestrator/tools_kb.py +156 -0
- src/orchestrator/tools_siem.py +1709 -0
- src/web/__init__.py +8 -0
- 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
|
+
|