@misterhuydo/sentinel 1.5.60 → 1.5.62

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-04-21T08:26:07.121Z",
3
- "checkpoint_at": "2026-04-21T08:26:07.122Z",
2
+ "message": "Auto-checkpoint at 2026-04-21T09:35:34.502Z",
3
+ "checkpoint_at": "2026-04-21T09:35:34.503Z",
4
4
  "active_files": [
5
5
  "J:\\Projects\\Sentinel\\cli\\bin\\sentinel.js",
6
6
  "J:\\Projects\\Sentinel\\cli\\lib\\test.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.5.60",
3
+ "version": "1.5.62",
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.60"
1
+ __version__ = "1.5.62"
@@ -1770,6 +1770,44 @@ def _format_monitor_step_output(tool: str, raw: str) -> str | None:
1770
1770
  text += f"\n_…and {len(matches) - 200} more_"
1771
1771
  return f"```\n{text}\n```"
1772
1772
 
1773
+ # ── search_logs ───────────────────────────────────────────────────────────
1774
+ if tool == "search_logs":
1775
+ hits = data.get("results") or data.get("matches") or []
1776
+ if not hits:
1777
+ return None
1778
+ lines = [
1779
+ f"[{h.get('source','?')}] {h.get('line', h)}" if isinstance(h, dict) else str(h)
1780
+ for h in hits[:200]
1781
+ ]
1782
+ text = "\n".join(lines)
1783
+ if len(hits) > 200:
1784
+ text += f"\n_…and {len(hits) - 200} more_"
1785
+ return f"```\n{text}\n```"
1786
+
1787
+ # ── list_pending_prs ──────────────────────────────────────────────────────
1788
+ if tool == "list_pending_prs":
1789
+ prs = data.get("prs") or data.get("results") or []
1790
+ if not prs:
1791
+ return None
1792
+ lines = [
1793
+ f"• [{p.get('repo','?')}] {p.get('title','?')} — {p.get('url') or p.get('pr_url','')}"
1794
+ if isinstance(p, dict) else str(p)
1795
+ for p in prs
1796
+ ]
1797
+ return "\n".join(lines)
1798
+
1799
+ # ── get_repo_status ───────────────────────────────────────────────────────
1800
+ if tool == "get_repo_status":
1801
+ repos = data.get("repos") or (data.get("repo") and [data]) or []
1802
+ if not repos:
1803
+ return raw.strip() or None
1804
+ lines = []
1805
+ for r in repos:
1806
+ name = r.get("repo") or r.get("name", "?")
1807
+ status = r.get("status") or r.get("summary", "")
1808
+ lines.append(f"• *{name}*: {status}")
1809
+ return "\n".join(lines) or None
1810
+
1773
1811
  # ── get_status / check_health / others — always show ─────────────────────
1774
1812
  if "error" in data:
1775
1813
  return f":warning: `{tool}` error: {data['error']}"
@@ -1846,28 +1884,47 @@ async def _execute_monitor(monitor: dict, cfg_loader: ConfigLoader, store: State
1846
1884
  # Only post if there is something to show
1847
1885
  if formatted_parts:
1848
1886
  combined = "\n".join(formatted_parts)
1849
- # Slack section blocks are capped at 3000 chars — keep content well under
1850
- MAX_LEN = 2900
1851
- if len(combined) > MAX_LEN:
1852
- tail = combined[:MAX_LEN]
1853
- # Close any unclosed code block before the truncation note
1854
- if tail.count("```") % 2 == 1:
1855
- tail += "\n```"
1856
- combined = tail + f"\n_…truncated ({len(combined)} chars total)_"
1857
1887
  header = f":repeat: *Monitor `{mon_id}`* ({mon_name}) — run #{runs_after}"
1858
1888
  if done:
1859
1889
  header += " _(final)_"
1860
- try:
1861
- await slack_client.chat_postMessage(
1862
- channel=channel,
1863
- text=header, # plain-text fallback for notifications
1864
- blocks=[
1865
- {"type": "section", "text": {"type": "mrkdwn", "text": header}},
1866
- {"type": "section", "text": {"type": "mrkdwn", "text": combined}},
1867
- ],
1868
- )
1869
- except Exception as e:
1870
- logger.warning("Monitor %s: Slack post failed: %s", mon_id, e)
1890
+
1891
+ # Split into 2900-char chunks (Slack section block limit is 3000).
1892
+ # Chunk on line boundaries where possible; preserve open/close ``` pairs.
1893
+ CHUNK = 2900
1894
+ chunks: list[str] = []
1895
+ remaining = combined
1896
+ while remaining:
1897
+ if len(remaining) <= CHUNK:
1898
+ chunks.append(remaining)
1899
+ break
1900
+ # Find last newline within the limit
1901
+ split_at = remaining.rfind("\n", 0, CHUNK)
1902
+ if split_at == -1:
1903
+ split_at = CHUNK
1904
+ piece = remaining[:split_at]
1905
+ rest = remaining[split_at:].lstrip("\n")
1906
+ # Close any unclosed code block so each chunk is self-contained,
1907
+ # and reopen it at the start of the next chunk
1908
+ if piece.count("```") % 2 == 1:
1909
+ piece += "\n```"
1910
+ rest = "```\n" + rest
1911
+ chunks.append(piece)
1912
+ remaining = rest
1913
+
1914
+ for i, chunk in enumerate(chunks):
1915
+ chunk_header = header if i == 0 else f"_{mon_name} continued ({i + 1}/{len(chunks)})_"
1916
+ try:
1917
+ await slack_client.chat_postMessage(
1918
+ channel=channel,
1919
+ text=chunk_header,
1920
+ blocks=[
1921
+ {"type": "section", "text": {"type": "mrkdwn", "text": chunk_header}},
1922
+ {"type": "section", "text": {"type": "mrkdwn", "text": chunk}},
1923
+ ],
1924
+ )
1925
+ except Exception as e:
1926
+ logger.warning("Monitor %s: Slack post chunk %d failed: %s", mon_id, i, e)
1927
+ break
1871
1928
 
1872
1929
  if done:
1873
1930
  try:
@@ -374,8 +374,18 @@ reply with a grouped summary like this:
374
374
  – "every X min for Y hours/days" → calculate stop_at = now + duration
375
375
  – "every X min for N times" → set max_runs=N
376
376
  – "every X min until <datetime>" → set stop_at
377
- Minimum interval: 60 seconds. Allowed tools: fetch_logs, filter_logs, get_status,
378
- ask_logs, list_recent_commits, check_health.
377
+ Minimum interval: 60 seconds.
378
+ When a user asks what monitors can do or what tools are available for monitors, list all
379
+ allowed tools with a one-line description of each use case:
380
+ • fetch_logs — pull fresh logs from SSH servers (with optional filter/grep)
381
+ • filter_logs — search already-fetched logs by pattern
382
+ • ask_logs — ask an AI question about fetched log content
383
+ • get_status — Sentinel's error/fix summary for the last N hours
384
+ • check_health — health check on a service or repo
385
+ • list_recent_commits — recent commits across monitored repos
386
+ • search_logs — search indexed log history for a pattern across all sources
387
+ • list_pending_prs — show open Sentinel PRs waiting for review
388
+ • get_repo_status — git status of a repo (behind main, diverged, dirty, etc.)
379
389
  Always confirm to the user with the monitor ID and stop condition before creating.
380
390
  • `stop_monitor` — delete a monitor by ID (stops it if active); pass "all" to delete all in this channel
381
391
  • `list_monitors` — show active monitors plus completed/cancelled ones from the last 24 hours
@@ -1347,14 +1357,16 @@ _TOOLS = [
1347
1357
  "results to this Slack channel. Supports: run indefinitely (until stopped), run for "
1348
1358
  "a fixed duration (stop_at), or run N times (max_runs). "
1349
1359
  "steps is a list of {tool, inputs} objects — most monitors are a single step. "
1350
- "Allowed tools: fetch_logs, filter_logs, get_status, ask_logs, list_recent_commits, check_health. "
1360
+ "Allowed tools: fetch_logs, filter_logs, get_status, ask_logs, list_recent_commits, "
1361
+ "check_health, search_logs, list_pending_prs, get_repo_status. "
1351
1362
  "Boss calculates stop_at from phrases like 'within 2 hours' / 'for 30 minutes' using "
1352
1363
  "the current UTC time in the system prompt. "
1353
1364
  "IMPORTANT: ALWAYS call list_monitors FIRST to get the live DB state before calling "
1354
1365
  "this tool — never infer active monitors from conversation history, as finished monitors "
1355
1366
  "are deleted and context window state will be stale. "
1356
1367
  "Examples: 'fetch SSOLWA logs filtered by provision/phone every 5 min for 2 hours', "
1357
- "'check STS health every 10 min until I say stop', 'get status every hour for 3 times'."
1368
+ "'check STS health every 10 min until I say stop', 'search logs for OOM every 10 min', "
1369
+ "'watch for pending PRs every 30 min', 'monitor repo drift every hour'."
1358
1370
  ),
1359
1371
  "input_schema": {
1360
1372
  "type": "object",
@@ -3534,7 +3546,8 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
3534
3546
  return json.dumps({"error": f"interval_seconds must be >= 60 (minimum 1 minute), got {interval_s}"})
3535
3547
 
3536
3548
  _MONITOR_ALLOWED = {"fetch_logs", "filter_logs", "get_status", "ask_logs",
3537
- "list_recent_commits", "check_health"}
3549
+ "list_recent_commits", "check_health",
3550
+ "search_logs", "list_pending_prs", "get_repo_status"}
3538
3551
  for _step in steps:
3539
3552
  _t = _step.get("tool", "")
3540
3553
  if _t not in _MONITOR_ALLOWED: