@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.
package/.cairn/session.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"message": "Auto-checkpoint at 2026-04-
|
|
3
|
-
"checkpoint_at": "2026-04-
|
|
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 +1 @@
|
|
|
1
|
-
__version__ = "1.5.
|
|
1
|
+
__version__ = "1.5.62"
|
package/python/sentinel/main.py
CHANGED
|
@@ -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
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
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.
|
|
378
|
-
|
|
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,
|
|
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', '
|
|
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:
|