delimit-cli 4.0.3 → 4.0.5
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.
- package/README.md +9 -242
- package/bin/delimit-cli.js +580 -15
- package/bin/delimit-setup.js +30 -2
- package/gateway/ai/agent_dispatch.py +34 -2
- package/gateway/ai/github_scanner.py +1 -1
- package/gateway/ai/ledger_manager.py +13 -3
- package/gateway/ai/ledger_propose.py +240 -0
- package/gateway/ai/loop_engine.py +175 -372
- package/gateway/ai/notify.py +700 -13
- package/gateway/ai/reddit_proxy.py +106 -0
- package/gateway/ai/reddit_scanner.py +34 -0
- package/gateway/ai/server.py +343 -81
- package/gateway/ai/siem_streaming.py +290 -0
- package/gateway/ai/social_daemon.py +189 -0
- package/gateway/ai/swarm.py +434 -0
- package/lib/continuity-resolver.js +325 -0
- package/lib/cross-model-hooks.js +212 -0
- package/lib/delimit-template.js +5 -0
- package/lib/session-shell.js +655 -0
- package/lib/session-worker.js +479 -0
- package/package.json +1 -1
- package/scripts/security-check.sh +12 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""SIEM Streaming — forward audit trail events to external security tools.
|
|
2
|
+
|
|
3
|
+
LED-280: Enterprise CISOs need audit logs in their existing SIEM, not just our dashboard.
|
|
4
|
+
Supports Splunk HEC, Datadog Logs API, and AWS EventBridge.
|
|
5
|
+
|
|
6
|
+
Config stored at ~/.delimit/siem.json. Each integration can be enabled independently.
|
|
7
|
+
Events are forwarded in real-time as they're written to the audit trail.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("delimit.ai.siem_streaming")
|
|
18
|
+
|
|
19
|
+
SIEM_CONFIG_PATH = Path.home() / ".delimit" / "siem.json"
|
|
20
|
+
SIEM_LOG_PATH = Path.home() / ".delimit" / "siem_delivery.jsonl"
|
|
21
|
+
|
|
22
|
+
DEFAULT_CONFIG = {
|
|
23
|
+
"splunk": {
|
|
24
|
+
"enabled": False,
|
|
25
|
+
"hec_url": "",
|
|
26
|
+
"hec_token": "",
|
|
27
|
+
"index": "delimit",
|
|
28
|
+
"source": "delimit-governance",
|
|
29
|
+
"sourcetype": "_json",
|
|
30
|
+
},
|
|
31
|
+
"datadog": {
|
|
32
|
+
"enabled": False,
|
|
33
|
+
"api_key": "",
|
|
34
|
+
"site": "datadoghq.com",
|
|
35
|
+
"service": "delimit",
|
|
36
|
+
"source": "delimit-governance",
|
|
37
|
+
"tags": ["env:production"],
|
|
38
|
+
},
|
|
39
|
+
"eventbridge": {
|
|
40
|
+
"enabled": False,
|
|
41
|
+
"bus_name": "default",
|
|
42
|
+
"region": "us-east-1",
|
|
43
|
+
"source": "delimit.governance",
|
|
44
|
+
"detail_type": "GovernanceEvent",
|
|
45
|
+
},
|
|
46
|
+
"webhook": {
|
|
47
|
+
"enabled": False,
|
|
48
|
+
"url": "",
|
|
49
|
+
"headers": {},
|
|
50
|
+
"method": "POST",
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _load_config() -> Dict[str, Any]:
|
|
56
|
+
if SIEM_CONFIG_PATH.exists():
|
|
57
|
+
try:
|
|
58
|
+
return json.loads(SIEM_CONFIG_PATH.read_text())
|
|
59
|
+
except json.JSONDecodeError:
|
|
60
|
+
pass
|
|
61
|
+
return dict(DEFAULT_CONFIG)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _save_config(config: Dict[str, Any]) -> None:
|
|
65
|
+
SIEM_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
SIEM_CONFIG_PATH.write_text(json.dumps(config, indent=2))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _log_delivery(integration: str, event_id: str, status: str, error: str = "") -> None:
|
|
70
|
+
try:
|
|
71
|
+
SIEM_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
entry = {
|
|
73
|
+
"timestamp": time.time(),
|
|
74
|
+
"integration": integration,
|
|
75
|
+
"event_id": event_id,
|
|
76
|
+
"status": status,
|
|
77
|
+
"error": error,
|
|
78
|
+
}
|
|
79
|
+
with open(SIEM_LOG_PATH, "a") as f:
|
|
80
|
+
f.write(json.dumps(entry) + "\n")
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def configure(
|
|
86
|
+
integration: str,
|
|
87
|
+
settings: Optional[Dict[str, Any]] = None,
|
|
88
|
+
enabled: Optional[bool] = None,
|
|
89
|
+
) -> Dict[str, Any]:
|
|
90
|
+
"""Configure a SIEM integration.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
integration: splunk, datadog, eventbridge, or webhook
|
|
94
|
+
settings: Key-value pairs to update (e.g. {"hec_url": "...", "hec_token": "..."})
|
|
95
|
+
enabled: Enable or disable the integration
|
|
96
|
+
"""
|
|
97
|
+
if integration not in DEFAULT_CONFIG:
|
|
98
|
+
return {"error": f"Unknown integration: {integration}. Choose: splunk, datadog, eventbridge, webhook"}
|
|
99
|
+
|
|
100
|
+
config = _load_config()
|
|
101
|
+
if integration not in config:
|
|
102
|
+
config[integration] = dict(DEFAULT_CONFIG[integration])
|
|
103
|
+
|
|
104
|
+
if settings:
|
|
105
|
+
config[integration].update(settings)
|
|
106
|
+
if enabled is not None:
|
|
107
|
+
config[integration]["enabled"] = enabled
|
|
108
|
+
|
|
109
|
+
_save_config(config)
|
|
110
|
+
|
|
111
|
+
# Mask secrets in response
|
|
112
|
+
safe = dict(config[integration])
|
|
113
|
+
for key in ("hec_token", "api_key"):
|
|
114
|
+
if key in safe and safe[key]:
|
|
115
|
+
safe[key] = safe[key][:4] + "***" + safe[key][-4:]
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
"status": "configured",
|
|
119
|
+
"integration": integration,
|
|
120
|
+
"config": safe,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_status() -> Dict[str, Any]:
|
|
125
|
+
"""Get status of all SIEM integrations."""
|
|
126
|
+
config = _load_config()
|
|
127
|
+
integrations = {}
|
|
128
|
+
|
|
129
|
+
for name, settings in config.items():
|
|
130
|
+
if not isinstance(settings, dict):
|
|
131
|
+
continue
|
|
132
|
+
enabled = settings.get("enabled", False)
|
|
133
|
+
healthy = False
|
|
134
|
+
|
|
135
|
+
if enabled:
|
|
136
|
+
if name == "splunk":
|
|
137
|
+
healthy = bool(settings.get("hec_url") and settings.get("hec_token"))
|
|
138
|
+
elif name == "datadog":
|
|
139
|
+
healthy = bool(settings.get("api_key"))
|
|
140
|
+
elif name == "eventbridge":
|
|
141
|
+
healthy = bool(settings.get("bus_name"))
|
|
142
|
+
elif name == "webhook":
|
|
143
|
+
healthy = bool(settings.get("url"))
|
|
144
|
+
|
|
145
|
+
integrations[name] = {
|
|
146
|
+
"enabled": enabled,
|
|
147
|
+
"healthy": healthy if enabled else None,
|
|
148
|
+
"status": "active" if (enabled and healthy) else "misconfigured" if enabled else "disabled",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Delivery stats
|
|
152
|
+
delivery_count = 0
|
|
153
|
+
delivery_errors = 0
|
|
154
|
+
if SIEM_LOG_PATH.exists():
|
|
155
|
+
for line in SIEM_LOG_PATH.read_text().strip().split("\n")[-100:]:
|
|
156
|
+
try:
|
|
157
|
+
entry = json.loads(line)
|
|
158
|
+
delivery_count += 1
|
|
159
|
+
if entry.get("status") == "error":
|
|
160
|
+
delivery_errors += 1
|
|
161
|
+
except json.JSONDecodeError:
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
"integrations": integrations,
|
|
166
|
+
"active_count": sum(1 for i in integrations.values() if i["status"] == "active"),
|
|
167
|
+
"total_deliveries": delivery_count,
|
|
168
|
+
"delivery_errors": delivery_errors,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def forward_event(event: Dict[str, Any]) -> Dict[str, Any]:
|
|
173
|
+
"""Forward a governance event to all enabled SIEM integrations.
|
|
174
|
+
|
|
175
|
+
Called automatically by the audit trail when events are recorded.
|
|
176
|
+
Returns delivery status per integration.
|
|
177
|
+
"""
|
|
178
|
+
config = _load_config()
|
|
179
|
+
results = {}
|
|
180
|
+
event_id = event.get("id", str(time.time()))
|
|
181
|
+
|
|
182
|
+
for name, settings in config.items():
|
|
183
|
+
if not isinstance(settings, dict) or not settings.get("enabled"):
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
if name == "splunk":
|
|
188
|
+
results[name] = _forward_splunk(event, settings, event_id)
|
|
189
|
+
elif name == "datadog":
|
|
190
|
+
results[name] = _forward_datadog(event, settings, event_id)
|
|
191
|
+
elif name == "eventbridge":
|
|
192
|
+
results[name] = _forward_eventbridge(event, settings, event_id)
|
|
193
|
+
elif name == "webhook":
|
|
194
|
+
results[name] = _forward_webhook(event, settings, event_id)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
results[name] = {"status": "error", "error": str(e)}
|
|
197
|
+
_log_delivery(name, event_id, "error", str(e))
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
"event_id": event_id,
|
|
201
|
+
"forwarded_to": list(results.keys()),
|
|
202
|
+
"results": results,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _forward_splunk(event: Dict, settings: Dict, event_id: str) -> Dict:
|
|
207
|
+
"""Forward to Splunk via HTTP Event Collector (HEC)."""
|
|
208
|
+
import urllib.request
|
|
209
|
+
|
|
210
|
+
payload = json.dumps({
|
|
211
|
+
"event": event,
|
|
212
|
+
"source": settings.get("source", "delimit-governance"),
|
|
213
|
+
"sourcetype": settings.get("sourcetype", "_json"),
|
|
214
|
+
"index": settings.get("index", "delimit"),
|
|
215
|
+
}).encode()
|
|
216
|
+
|
|
217
|
+
req = urllib.request.Request(
|
|
218
|
+
settings["hec_url"],
|
|
219
|
+
data=payload,
|
|
220
|
+
headers={
|
|
221
|
+
"Authorization": f"Splunk {settings['hec_token']}",
|
|
222
|
+
"Content-Type": "application/json",
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
resp = urllib.request.urlopen(req, timeout=10)
|
|
226
|
+
_log_delivery("splunk", event_id, "ok")
|
|
227
|
+
return {"status": "ok", "http_code": resp.status}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _forward_datadog(event: Dict, settings: Dict, event_id: str) -> Dict:
|
|
231
|
+
"""Forward to Datadog Logs API."""
|
|
232
|
+
import urllib.request
|
|
233
|
+
|
|
234
|
+
site = settings.get("site", "datadoghq.com")
|
|
235
|
+
payload = json.dumps([{
|
|
236
|
+
"ddsource": settings.get("source", "delimit-governance"),
|
|
237
|
+
"ddtags": ",".join(settings.get("tags", [])),
|
|
238
|
+
"service": settings.get("service", "delimit"),
|
|
239
|
+
"message": json.dumps(event),
|
|
240
|
+
}]).encode()
|
|
241
|
+
|
|
242
|
+
req = urllib.request.Request(
|
|
243
|
+
f"https://http-intake.logs.{site}/api/v2/logs",
|
|
244
|
+
data=payload,
|
|
245
|
+
headers={
|
|
246
|
+
"DD-API-KEY": settings["api_key"],
|
|
247
|
+
"Content-Type": "application/json",
|
|
248
|
+
},
|
|
249
|
+
)
|
|
250
|
+
resp = urllib.request.urlopen(req, timeout=10)
|
|
251
|
+
_log_delivery("datadog", event_id, "ok")
|
|
252
|
+
return {"status": "ok", "http_code": resp.status}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _forward_eventbridge(event: Dict, settings: Dict, event_id: str) -> Dict:
|
|
256
|
+
"""Forward to AWS EventBridge."""
|
|
257
|
+
try:
|
|
258
|
+
import boto3
|
|
259
|
+
client = boto3.client("events", region_name=settings.get("region", "us-east-1"))
|
|
260
|
+
response = client.put_events(Entries=[{
|
|
261
|
+
"Source": settings.get("source", "delimit.governance"),
|
|
262
|
+
"DetailType": settings.get("detail_type", "GovernanceEvent"),
|
|
263
|
+
"Detail": json.dumps(event),
|
|
264
|
+
"EventBusName": settings.get("bus_name", "default"),
|
|
265
|
+
}])
|
|
266
|
+
failed = response.get("FailedEntryCount", 0)
|
|
267
|
+
status = "ok" if failed == 0 else "partial"
|
|
268
|
+
_log_delivery("eventbridge", event_id, status)
|
|
269
|
+
return {"status": status, "failed_entries": failed}
|
|
270
|
+
except ImportError:
|
|
271
|
+
_log_delivery("eventbridge", event_id, "error", "boto3 not installed")
|
|
272
|
+
return {"status": "error", "error": "boto3 not installed. Run: pip install boto3"}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _forward_webhook(event: Dict, settings: Dict, event_id: str) -> Dict:
|
|
276
|
+
"""Forward to a generic webhook URL."""
|
|
277
|
+
import urllib.request
|
|
278
|
+
|
|
279
|
+
headers = {"Content-Type": "application/json"}
|
|
280
|
+
headers.update(settings.get("headers", {}))
|
|
281
|
+
|
|
282
|
+
req = urllib.request.Request(
|
|
283
|
+
settings["url"],
|
|
284
|
+
data=json.dumps(event).encode(),
|
|
285
|
+
headers=headers,
|
|
286
|
+
method=settings.get("method", "POST"),
|
|
287
|
+
)
|
|
288
|
+
resp = urllib.request.urlopen(req, timeout=10)
|
|
289
|
+
_log_delivery("webhook", event_id, "ok")
|
|
290
|
+
return {"status": "ok", "http_code": resp.status}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Social sensing daemon for Delimit.
|
|
3
|
+
|
|
4
|
+
Runs social discovery scans (X, Reddit, GitHub, Dev.to) on a regular interval.
|
|
5
|
+
Deduplicates findings and emits HTML draft emails for human approval.
|
|
6
|
+
Also monitors for direct replies to owned posts (LED-300).
|
|
7
|
+
|
|
8
|
+
Consensus 123: Part of the continuous sensing loop.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("delimit.ai.social_daemon")
|
|
21
|
+
|
|
22
|
+
# ── Configuration ────────────────────────────────────────────────────
|
|
23
|
+
# Default to 15 minutes (900 seconds)
|
|
24
|
+
SCAN_INTERVAL = int(os.environ.get("DELIMIT_SOCIAL_SCAN_INTERVAL", "900"))
|
|
25
|
+
MAX_CONSECUTIVE_FAILURES = 3
|
|
26
|
+
|
|
27
|
+
ALERTS_DIR = Path.home() / ".delimit" / "alerts"
|
|
28
|
+
ALERT_FILE = ALERTS_DIR / "social_daemon.json"
|
|
29
|
+
DAEMON_STATE = Path.home() / ".delimit" / "social_daemon_state.json"
|
|
30
|
+
OWNER_ACTION_SUMMARY = Path.home() / ".delimit" / "owner_action_summary.json"
|
|
31
|
+
|
|
32
|
+
class SocialDaemonState:
|
|
33
|
+
"""Thread-safe state for the social sensing daemon."""
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self.running = False
|
|
37
|
+
self.last_scan: Optional[str] = None
|
|
38
|
+
self.targets_found: int = 0
|
|
39
|
+
self.consecutive_failures: int = 0
|
|
40
|
+
self.total_scans: int = 0
|
|
41
|
+
self.stopped_reason: Optional[str] = None
|
|
42
|
+
self._lock = threading.Lock()
|
|
43
|
+
self._thread: Optional[threading.Thread] = None
|
|
44
|
+
self._stop_event = threading.Event()
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
47
|
+
with self._lock:
|
|
48
|
+
return {
|
|
49
|
+
"running": self.running,
|
|
50
|
+
"last_scan": self.last_scan,
|
|
51
|
+
"targets_found": self.targets_found,
|
|
52
|
+
"consecutive_failures": self.consecutive_failures,
|
|
53
|
+
"total_scans": self.total_scans,
|
|
54
|
+
"stopped_reason": self.stopped_reason,
|
|
55
|
+
"scan_interval_seconds": SCAN_INTERVAL,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def record_success(self, found: int):
|
|
59
|
+
with self._lock:
|
|
60
|
+
self.consecutive_failures = 0
|
|
61
|
+
self.targets_found += found
|
|
62
|
+
self.total_scans += 1
|
|
63
|
+
self.last_scan = datetime.now(timezone.utc).isoformat()
|
|
64
|
+
|
|
65
|
+
def record_failure(self) -> int:
|
|
66
|
+
with self._lock:
|
|
67
|
+
self.consecutive_failures += 1
|
|
68
|
+
self.total_scans += 1
|
|
69
|
+
self.last_scan = datetime.now(timezone.utc).isoformat()
|
|
70
|
+
return self.consecutive_failures
|
|
71
|
+
|
|
72
|
+
_daemon_state = SocialDaemonState()
|
|
73
|
+
|
|
74
|
+
def scan_once() -> Dict[str, Any]:
|
|
75
|
+
"""Execute a single social scan cycle and process results (LED-238)."""
|
|
76
|
+
from ai.social_target import scan_targets, process_targets
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
# 1. DISCOVER: Scan all platforms
|
|
80
|
+
targets = scan_targets(platforms=["x", "reddit", "github", "hn", "devto"])
|
|
81
|
+
found = len(targets)
|
|
82
|
+
|
|
83
|
+
# 2. ORCHESTRATE: Process discovered targets (LED-238)
|
|
84
|
+
# draft_replies=True -> emits social_draft emails
|
|
85
|
+
# create_ledger=True -> creates strategic ledger items
|
|
86
|
+
processed = process_targets(targets, draft_replies=True, create_ledger=True)
|
|
87
|
+
OWNER_ACTION_SUMMARY.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
OWNER_ACTION_SUMMARY.write_text(json.dumps({
|
|
89
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
90
|
+
"targets_found": found,
|
|
91
|
+
"owner_actions": len(processed.get("owner_actions", [])),
|
|
92
|
+
"drafted": len(processed.get("drafted", [])),
|
|
93
|
+
"ledger_items": len(processed.get("ledger_items", [])),
|
|
94
|
+
"strategy_items": len(processed.get("strategy_items", [])),
|
|
95
|
+
}, indent=2) + "\n")
|
|
96
|
+
|
|
97
|
+
_daemon_state.record_success(found)
|
|
98
|
+
return {
|
|
99
|
+
"targets_found": found,
|
|
100
|
+
"processed": processed
|
|
101
|
+
}
|
|
102
|
+
except Exception as e:
|
|
103
|
+
failures = _daemon_state.record_failure()
|
|
104
|
+
logger.error("Social scan failed: %s", e)
|
|
105
|
+
if failures >= MAX_CONSECUTIVE_FAILURES:
|
|
106
|
+
reason = f"3 consecutive social scan failures. Last: {e}"
|
|
107
|
+
_daemon_state.stopped_reason = reason
|
|
108
|
+
_daemon_state.running = False
|
|
109
|
+
_daemon_state._stop_event.set()
|
|
110
|
+
return {"error": str(e), "consecutive_failures": failures}
|
|
111
|
+
|
|
112
|
+
def _daemon_loop() -> None:
|
|
113
|
+
"""Main scanning loop."""
|
|
114
|
+
logger.info("Social daemon started. Scanning every %d seconds.", SCAN_INTERVAL)
|
|
115
|
+
|
|
116
|
+
while not _daemon_state._stop_event.is_set():
|
|
117
|
+
try:
|
|
118
|
+
result = scan_once()
|
|
119
|
+
if "error" in result:
|
|
120
|
+
logger.warning("Scan cycle error: %s", result["error"])
|
|
121
|
+
else:
|
|
122
|
+
logger.info("Scan complete: %d new targets", result.get("targets_found", 0))
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error("Unexpected error in social daemon loop: %s", e)
|
|
125
|
+
failures = _daemon_state.record_failure()
|
|
126
|
+
if failures >= MAX_CONSECUTIVE_FAILURES:
|
|
127
|
+
break
|
|
128
|
+
|
|
129
|
+
_daemon_state._stop_event.wait(timeout=SCAN_INTERVAL)
|
|
130
|
+
|
|
131
|
+
_daemon_state.running = False
|
|
132
|
+
logger.info("Social daemon stopped.")
|
|
133
|
+
|
|
134
|
+
def start_daemon() -> Dict[str, Any]:
|
|
135
|
+
"""Start the social daemon in a background thread."""
|
|
136
|
+
if _daemon_state.running:
|
|
137
|
+
return {"status": "already_running", **_daemon_state.to_dict()}
|
|
138
|
+
|
|
139
|
+
_daemon_state.running = True
|
|
140
|
+
_daemon_state.stopped_reason = None
|
|
141
|
+
_daemon_state.consecutive_failures = 0
|
|
142
|
+
_daemon_state._stop_event.clear()
|
|
143
|
+
|
|
144
|
+
thread = threading.Thread(target=_daemon_loop, name="social-daemon", daemon=True)
|
|
145
|
+
_daemon_state._thread = thread
|
|
146
|
+
thread.start()
|
|
147
|
+
|
|
148
|
+
return {"status": "started", **_daemon_state.to_dict()}
|
|
149
|
+
|
|
150
|
+
def stop_daemon() -> Dict[str, Any]:
|
|
151
|
+
"""Stop the social daemon."""
|
|
152
|
+
if not _daemon_state.running:
|
|
153
|
+
return {"status": "not_running", **_daemon_state.to_dict()}
|
|
154
|
+
|
|
155
|
+
_daemon_state._stop_event.set()
|
|
156
|
+
_daemon_state.stopped_reason = "manual_stop"
|
|
157
|
+
if _daemon_state._thread:
|
|
158
|
+
_daemon_state._thread.join(timeout=5)
|
|
159
|
+
_daemon_state.running = False
|
|
160
|
+
|
|
161
|
+
return {"status": "stopped", **_daemon_state.to_dict()}
|
|
162
|
+
|
|
163
|
+
def get_daemon_status() -> Dict[str, Any]:
|
|
164
|
+
"""Get current daemon status."""
|
|
165
|
+
return _daemon_state.to_dict()
|
|
166
|
+
|
|
167
|
+
def main():
|
|
168
|
+
"""Run as standalone process."""
|
|
169
|
+
import argparse
|
|
170
|
+
parser = argparse.ArgumentParser(description="Delimit social sensing daemon")
|
|
171
|
+
parser.add_argument("--interval", type=int, help="Scan interval in seconds")
|
|
172
|
+
parser.add_argument("--once", action="store_true", help="Run once and exit")
|
|
173
|
+
args = parser.parse_args()
|
|
174
|
+
|
|
175
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s: %(message)s")
|
|
176
|
+
|
|
177
|
+
if args.interval:
|
|
178
|
+
global SCAN_INTERVAL
|
|
179
|
+
SCAN_INTERVAL = args.interval
|
|
180
|
+
|
|
181
|
+
if args.once:
|
|
182
|
+
scan_once()
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
_daemon_state.running = True
|
|
186
|
+
_daemon_loop()
|
|
187
|
+
|
|
188
|
+
if __name__ == "__main__":
|
|
189
|
+
main()
|