@misterhuydo/sentinel 1.5.42 → 1.5.44

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.5.42",
3
+ "version": "1.5.44",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -1 +1 @@
1
- __version__ = "1.5.42"
1
+ __version__ = "1.5.44"
@@ -105,6 +105,7 @@ class LogSourceConfig:
105
105
  cf_url: str = ""
106
106
  cf_token: str = ""
107
107
  sync_enabled: bool = True
108
+ health_url: str = "" # optional HTTP health endpoint for this log source's service
108
109
 
109
110
 
110
111
  @dataclass
@@ -308,6 +309,7 @@ class ConfigLoader:
308
309
  s.cf_token = d.get("CF_TOKEN", "")
309
310
  s.target_repo = d.get("TARGET_REPO", "auto")
310
311
  s.sync_enabled = d.get("SYNC_ENABLED", "true").lower() != "false"
312
+ s.health_url = d.get("HEALTH_URL", "")
311
313
  self.log_sources[s.name] = s
312
314
 
313
315
  def _load_repos(self):
@@ -157,7 +157,7 @@ COMPLETE TOOL REFERENCE
157
157
  custom fetch, and filter_logs searches them automatically.
158
158
  Use grep_filter for INFO-level or feature-specific patterns that the default
159
159
  WARN/ERROR filter would miss.
160
- "fetch logs", "fetch SSOLWA with filter provision/phone", "fetch without filter"
160
+ "fetch logs", "fetch logs for <source> with filter <pattern>", "fetch without filter"
161
161
 
162
162
  7. search_logs Live SSH grep on production servers using GREP_FILTER.
163
163
  Falls back to cached files if SSH unavailable.
@@ -171,7 +171,11 @@ COMPLETE TOOL REFERENCE
171
171
  9. tail_log Last N lines of a log source live, no filter.
172
172
  "show recent SSOLWA logs", "tail STS", "last 200 lines from 1881"
173
173
 
174
- 10. ask_logs Ask Claude Code to read and reason over log history.
174
+ 10. check_health Call HEALTH_URL for a log source or repo returns live version + status.
175
+ Use whenever deployment status needs verification. NEVER guess from logs.
176
+ "is version X deployed?", "what version is <service> running?", "is the service up?"
177
+
178
+ 11. ask_logs Ask Claude Code to read and reason over log history.
175
179
  Use for summarisation, pattern detection, trend analysis.
176
180
  "what caused 400s in 1881 logs?", "summarise last week of STS logs"
177
181
 
@@ -576,9 +580,10 @@ CORRECT:
576
580
  "No '<feature log pattern>' lines yet — the code path hasn't been triggered since the last
577
581
  fetch. This says nothing about whether the release is deployed."
578
582
 
579
- To verify if a release is live, offer to search for startup log lines:
580
- search_logs with query "Starting|started in|version|initialized"
581
- NEVER repeat a deployment assertion from a prior turn without doing this check first.
583
+ To verify if a release is live:
584
+ 1. FIRST: call check_health for the relevant source/repo — it returns the live version directly.
585
+ 2. If no HEALTH_URL is configured: search_logs with query "Starting|started in|version|initialized".
586
+ NEVER state deployment status without calling check_health first.
582
587
  - If a tool call will take a moment (search, fetch, pull), prefix your reply with a brief "working" line ending in "..." before the results, e.g. "Searching SSOLWA for TryDig activity..." then the actual output.
583
588
  Never just say a working line and stop — always follow it with the results in the same message.
584
589
 
@@ -1443,6 +1448,30 @@ _TOOLS = [
1443
1448
  "required": ["question"],
1444
1449
  },
1445
1450
  },
1451
+ {
1452
+ "name": "check_health",
1453
+ "description": (
1454
+ "Call the HEALTH_URL for a log source or repo and return the raw JSON response. "
1455
+ "Use this to verify whether a service is running and what version it reports. "
1456
+ "ALWAYS call this instead of guessing deployment status from log evidence. "
1457
+ "Use for: 'is version X deployed?', 'what version is the service running?', "
1458
+ "'is the service up?', 'verify the release is live'."
1459
+ ),
1460
+ "input_schema": {
1461
+ "type": "object",
1462
+ "properties": {
1463
+ "source": {
1464
+ "type": "string",
1465
+ "description": "Log source name (e.g. 'SSOLWA') or repo name to check. "
1466
+ "Checks HEALTH_URL from that source/repo config.",
1467
+ },
1468
+ "url": {
1469
+ "type": "string",
1470
+ "description": "Direct health URL to call (overrides config). Use when the user provides a URL.",
1471
+ },
1472
+ },
1473
+ },
1474
+ },
1446
1475
  {
1447
1476
  "name": "post_file",
1448
1477
  "description": (
@@ -2040,6 +2069,36 @@ def _filter_log_sources(props_files: list, source_hint: str) -> list:
2040
2069
  return matched
2041
2070
 
2042
2071
 
2072
+ def _auto_health_check(source_hint: str, cfg_loader) -> dict | None:
2073
+ """
2074
+ If any log source or repo matching source_hint has a HEALTH_URL, call it and return
2075
+ the result so it can be injected into fetch/filter responses automatically.
2076
+ Returns None if no health URL is configured or the call fails silently.
2077
+ """
2078
+ try:
2079
+ import requests as _req
2080
+ url = ""
2081
+ for src_name, src in cfg_loader.log_sources.items():
2082
+ if source_hint.lower() in src_name.lower() and getattr(src, "health_url", ""):
2083
+ url = src.health_url
2084
+ break
2085
+ if not url:
2086
+ for repo_name, repo in cfg_loader.repos.items():
2087
+ if source_hint.lower() in repo_name.lower() and repo.health_url:
2088
+ url = repo.health_url
2089
+ break
2090
+ if not url:
2091
+ return None
2092
+ resp = _req.get(url, timeout=8)
2093
+ try:
2094
+ data = resp.json()
2095
+ except Exception:
2096
+ data = resp.text[:500]
2097
+ return {"health_url": url, "status_code": resp.status_code, "health": data}
2098
+ except Exception:
2099
+ return None
2100
+
2101
+
2043
2102
  # ── Tool execution ────────────────────────────────────────────────────────────
2044
2103
 
2045
2104
  async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=None, user_id: str = "", channel: str = "", is_admin: bool = False) -> str:
@@ -2900,6 +2959,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2900
2959
  sources_searched = [d.name + (" [temp]" if is_temp else "") for d, is_temp in search_pairs]
2901
2960
  if total == 0:
2902
2961
  has_temp = bool(temp_dirs)
2962
+ health = _auto_health_check(source_f, cfg_loader) if source_f else None
2903
2963
  return json.dumps({
2904
2964
  "query": query_f,
2905
2965
  "total_matches": 0,
@@ -2910,6 +2970,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2910
2970
  + "If searching for a specific log line from a new feature, use fetch_logs "
2911
2971
  "with a matching grep_filter first — the default filter only captures WARN/ERROR."
2912
2972
  ),
2973
+ **({"service_health": health} if health else {}),
2913
2974
  })
2914
2975
 
2915
2976
  # Pattern grouping: count occurrences of each error signature
@@ -2951,6 +3012,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2951
3012
  except Exception:
2952
3013
  pass
2953
3014
 
3015
+ health = _auto_health_check(source_f, cfg_loader) if source_f else None
2954
3016
  return json.dumps({
2955
3017
  "query": query_f,
2956
3018
  "total_matches": total,
@@ -2960,6 +3022,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2960
3022
  "sample_lines": sample_lines,
2961
3023
  "time_span": time_span,
2962
3024
  "capped": total >= max_matches,
3025
+ **({"service_health": health} if health else {}),
2963
3026
  })
2964
3027
 
2965
3028
  if name == "trigger_poll":
@@ -3112,7 +3175,12 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
3112
3175
  except Exception as e:
3113
3176
  results.append({"source": props.stem, "error": str(e)})
3114
3177
 
3115
- return json.dumps({"fetched": len(results), "results": results})
3178
+ health = _auto_health_check(source_filter, cfg_loader) if source_filter else None
3179
+ return json.dumps({
3180
+ "fetched": len(results),
3181
+ "results": results,
3182
+ **({"service_health": health} if health else {}),
3183
+ })
3116
3184
 
3117
3185
  if name == "watch_bot":
3118
3186
  if not is_admin:
@@ -3552,6 +3620,50 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
3552
3620
  results.append({"project": d.name, "status": "error", "detail": str(e)})
3553
3621
  return json.dumps({"results": results})
3554
3622
 
3623
+ if name == "check_health":
3624
+ import requests as _requests
3625
+ url = inputs.get("url", "").strip()
3626
+ source_hint = inputs.get("source", "").strip().lower()
3627
+
3628
+ # Resolve URL from config if not provided directly
3629
+ if not url:
3630
+ # Check log sources first, then repos
3631
+ for src_name, src in cfg_loader.log_sources.items():
3632
+ if source_hint in src_name.lower() and getattr(src, "health_url", ""):
3633
+ url = src.health_url
3634
+ break
3635
+ if not url:
3636
+ for repo_name, repo in cfg_loader.repos.items():
3637
+ if source_hint in repo_name.lower() and repo.health_url:
3638
+ url = repo.health_url
3639
+ break
3640
+
3641
+ if not url:
3642
+ configured = {
3643
+ **{n: s.health_url for n, s in cfg_loader.log_sources.items() if getattr(s, "health_url", "")},
3644
+ **{n: r.health_url for n, r in cfg_loader.repos.items() if r.health_url},
3645
+ }
3646
+ return json.dumps({
3647
+ "error": f"No HEALTH_URL found for '{source_hint}'.",
3648
+ "configured_health_urls": configured or "none",
3649
+ "hint": "Add HEALTH_URL=https://... to the log-source or repo .properties file, "
3650
+ "or pass url= directly.",
3651
+ })
3652
+
3653
+ try:
3654
+ resp = _requests.get(url, timeout=10)
3655
+ try:
3656
+ data = resp.json()
3657
+ except Exception:
3658
+ data = resp.text[:2000]
3659
+ return json.dumps({
3660
+ "url": url,
3661
+ "status_code": resp.status_code,
3662
+ "response": data,
3663
+ })
3664
+ except Exception as e:
3665
+ return json.dumps({"url": url, "error": str(e)})
3666
+
3555
3667
  if name == "tail_log":
3556
3668
  source = inputs.get("source", "").lower()
3557
3669
  lines = int(inputs.get("lines", 100))