@misterhuydo/sentinel 1.0.64 → 1.0.66

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-03-22T15:44:29.732Z",
3
- "checkpoint_at": "2026-03-22T15:44:29.733Z",
2
+ "message": "Auto-checkpoint at 2026-03-22T16:04:55.232Z",
3
+ "checkpoint_at": "2026-03-22T16:04:55.233Z",
4
4
  "active_files": [],
5
5
  "notes": [],
6
6
  "mtime_snapshot": {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.0.64",
3
+ "version": "1.0.66",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -53,8 +53,9 @@ What you can do (tools available):
53
53
  5. list_projects — List all configured repos and log sources in this Sentinel instance.
54
54
  e.g. "what projects are you watching?", "list all repos"
55
55
 
56
- 6. search_logs — Search fetched log files for a pattern or keyword.
57
- e.g. "search logs for X", "find entries for user Y", "grep logs for Z"
56
+ 6. search_logs — SSH live to servers and grep logs in real time (uses fetch_log.sh with
57
+ the query as GREP_FILTER). Falls back to cached files if unavailable.
58
+ e.g. "search logs for illegal PIN in 1881", "find X in SSOLWA", "grep logs for Z"
58
59
 
59
60
  7. trigger_poll — Trigger an immediate poll cycle without waiting for the schedule.
60
61
  e.g. "check now", "poll immediately", "don't wait, run now"
@@ -109,7 +110,7 @@ reply with a short summary grouped by category:
109
110
 
110
111
  *Log management*
111
112
  • `fetch_logs` — pull fresh logs from servers right now — "fetch logs for SSOLWA"
112
- • `search_logs` — search fetched logs by keyword/regex — "search logs for NullPointerException"
113
+ • `search_logs` — live SSH grep on production servers — "search logs for illegal PIN in 1881"
113
114
 
114
115
  *Fix management*
115
116
  • `get_fix_details` — full details of a specific fix — "show fix abc123"
@@ -232,25 +233,28 @@ _TOOLS = [
232
233
  {
233
234
  "name": "search_logs",
234
235
  "description": (
235
- "Search fetched log files for a pattern. Use for: 'find activity from appid=X', "
236
- "'search logs for Y', 'show me log entries containing Z', 'what did user X do?'. "
237
- "Supports any regex or plain text pattern."
236
+ "Search production logs for a keyword or pattern. "
237
+ "When a project or source is specified (or can be inferred), performs a LIVE fetch "
238
+ "via fetch_log.sh with the query as the grep filter — SSHes directly to the server. "
239
+ "Falls back to searching locally-cached log files when no source can be determined. "
240
+ "Use for: 'search logs for illegal PIN in 1881', 'find X in SSOLWA logs', "
241
+ "'what did user Y do?', 'show entries for appid=Z', 'grep logs for X'."
238
242
  ),
239
243
  "input_schema": {
240
244
  "type": "object",
241
245
  "properties": {
242
246
  "query": {
243
247
  "type": "string",
244
- "description": "Regex or plain text to search for",
248
+ "description": "Keyword or regex to grep for",
245
249
  },
246
250
  "source": {
247
251
  "type": "string",
248
- "description": "Optional: limit to a specific log source name (partial match)",
252
+ "description": "Log source name to search (partial match against log-config filenames, e.g. 'SSOLWA', '1881'). Leave empty to search all sources.",
249
253
  },
250
254
  "max_matches": {
251
255
  "type": "integer",
252
- "description": "Max matching lines to return per file (default 20)",
253
- "default": 20,
256
+ "description": "Max matching lines to return per source (default 30)",
257
+ "default": 30,
254
258
  },
255
259
  },
256
260
  "required": ["query"],
@@ -634,10 +638,46 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
634
638
  if name == "search_logs":
635
639
  query = inputs.get("query", "")
636
640
  source = inputs.get("source", "").lower()
637
- max_matches = int(inputs.get("max_matches", 20))
641
+ max_matches = int(inputs.get("max_matches", 30))
642
+
643
+ # ── Live fetch path: SSH to servers and grep in real time ──────────────
644
+ script = Path(__file__).resolve().parent.parent / "scripts" / "fetch_log.sh"
645
+ log_cfg_dir = Path("config") / "log-configs"
646
+ if script.exists() and log_cfg_dir.exists():
647
+ props_files = sorted(log_cfg_dir.glob("*.properties"))
648
+ if source:
649
+ props_files = [p for p in props_files if source in p.stem.lower()]
650
+ if props_files:
651
+ live_results = []
652
+ for props in props_files:
653
+ env = os.environ.copy()
654
+ env["GREP_FILTER"] = query
655
+ try:
656
+ r = subprocess.run(
657
+ ["bash", str(script), str(props)],
658
+ capture_output=True, text=True, timeout=60, env=env,
659
+ )
660
+ lines = (r.stdout or "").strip().splitlines()
661
+ matches = [ln[:300] for ln in lines if ln.strip()][:max_matches]
662
+ if matches:
663
+ live_results.append({"source": props.stem, "matches": matches})
664
+ logger.info("Boss search_logs live %s rc=%d found=%d", props.stem, r.returncode, len(matches))
665
+ except subprocess.TimeoutExpired:
666
+ live_results.append({"source": props.stem, "error": "timed out"})
667
+ except Exception as e:
668
+ live_results.append({"source": props.stem, "error": str(e)})
669
+ total = sum(len(r.get("matches", [])) for r in live_results)
670
+ return json.dumps({
671
+ "query": query,
672
+ "mode": "live",
673
+ "total_matches": total,
674
+ "results": live_results,
675
+ })
676
+
677
+ # ── Fallback: search locally-cached log files ──────────────────────────
638
678
  fetched_dir = Path("workspace/fetched")
639
679
  if not fetched_dir.exists():
640
- return json.dumps({"error": "No fetched logs found run a poll first"})
680
+ return json.dumps({"error": "No fetched logs found and fetch_log.sh unavailable"})
641
681
  try:
642
682
  pattern = re.compile(query, re.IGNORECASE)
643
683
  except re.error as e:
@@ -660,6 +700,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
660
700
  total = sum(len(r["matches"]) for r in results)
661
701
  return json.dumps({
662
702
  "query": query,
703
+ "mode": "cached",
663
704
  "total_matches": total,
664
705
  "files_searched": len(list(fetched_dir.glob("*.log"))),
665
706
  "results": results,
@@ -938,9 +979,10 @@ async def _handle_with_cli(
938
979
  query = quoted[0] if quoted else message
939
980
  search_json = await _run_tool("search_logs", {"query": query}, cfg_loader, store)
940
981
 
941
- paused = Path("SENTINEL_PAUSE").exists()
942
- repos = list(cfg_loader.repos.keys())
943
- ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
982
+ paused = Path("SENTINEL_PAUSE").exists()
983
+ repos = list(cfg_loader.repos.keys())
984
+ log_sources = list(cfg_loader.log_sources.keys())
985
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
944
986
 
945
987
  history_text = ""
946
988
  for msg in history[-8:]:
@@ -961,6 +1003,7 @@ async def _handle_with_cli(
961
1003
  + f"\n\nCurrent time: {ts}"
962
1004
  + f"\nSentinel status: {'⏸ PAUSED' if paused else '▶ RUNNING'}"
963
1005
  + f"\nManaged repos: {', '.join(repos) if repos else '(none configured)'}"
1006
+ + (f"\nLog sources: {', '.join(log_sources)}" if log_sources else "")
964
1007
  + f"\n\nCurrent status (last 24 h):\n{status_json}"
965
1008
  + f"\n\nOpen PRs:\n{prs_json}"
966
1009
  + (f"\n\nLog search results:\n{search_json}" if search_json else "")
@@ -1063,12 +1106,14 @@ async def handle_message(
1063
1106
  repos = list(cfg_loader.repos.keys())
1064
1107
  ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
1065
1108
  known_projects = [_read_project_name(d) for d in _find_project_dirs()]
1109
+ log_sources = list(cfg_loader.log_sources.keys())
1066
1110
  system = (
1067
1111
  _SYSTEM
1068
1112
  + (f"\nYou are speaking with: {user_name}" if user_name else "")
1069
1113
  + f"\n\nCurrent time: {ts}"
1070
1114
  + f"\nSentinel status: {'⏸ PAUSED' if paused else '▶ RUNNING'}"
1071
1115
  + f"\nManaged repos: {', '.join(repos) if repos else '(none configured)'}"
1116
+ + (f"\nLog sources: {', '.join(log_sources)}" if log_sources else "")
1072
1117
  + (f"\nKnown projects in workspace: {', '.join(known_projects)}" if known_projects else "")
1073
1118
  )
1074
1119