@misterhuydo/sentinel 1.5.56 → 1.5.58

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/.hint-lock CHANGED
@@ -1 +1 @@
1
- 2026-04-21T07:25:38.460Z
1
+ 2026-04-21T07:58:09.595Z
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-04-21T07:26:06.811Z",
3
- "checkpoint_at": "2026-04-21T07:26:06.813Z",
2
+ "message": "Auto-checkpoint at 2026-04-21T08:02:23.465Z",
3
+ "checkpoint_at": "2026-04-21T08:02:23.467Z",
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.56",
3
+ "version": "1.5.58",
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.56"
1
+ __version__ = "1.5.58"
@@ -1848,7 +1848,11 @@ async def _execute_monitor(monitor: dict, cfg_loader: ConfigLoader, store: State
1848
1848
  combined = "\n".join(formatted_parts)
1849
1849
  MAX_LEN = 3800
1850
1850
  if len(combined) > MAX_LEN:
1851
- combined = combined[:MAX_LEN] + f"\n_…truncated ({len(combined)} chars total)_"
1851
+ tail = combined[:MAX_LEN]
1852
+ # Close any unclosed code block before the truncation note
1853
+ if tail.count("```") % 2 == 1:
1854
+ tail += "\n```"
1855
+ combined = tail + f"\n_…truncated ({len(combined)} chars total)_"
1852
1856
  header = f":repeat: *Monitor `{mon_id}`* ({mon_name}) — run #{runs_after}"
1853
1857
  if done:
1854
1858
  header += " _(final)_"
@@ -377,8 +377,8 @@ reply with a grouped summary like this:
377
377
  Minimum interval: 60 seconds. Allowed tools: fetch_logs, filter_logs, get_status,
378
378
  ask_logs, list_recent_commits, check_health.
379
379
  Always confirm to the user with the monitor ID and stop condition before creating.
380
- • `stop_monitor` — cancel a monitor by ID, or pass "all" to cancel all in this channel
381
- • `list_monitors` — show all active monitors
380
+ • `stop_monitor` — delete a monitor by ID (stops it if active); pass "all" to delete all in this channel
381
+ • `list_monitors` — show active monitors plus completed/cancelled ones from the last 24 hours
382
382
 
383
383
  *File sharing*
384
384
  • `post_file` — upload any output as a Slack file (logs, diffs, reports)
@@ -1394,8 +1394,12 @@ _TOOLS = [
1394
1394
  {
1395
1395
  "name": "stop_monitor",
1396
1396
  "description": (
1397
- "Cancel a running monitor by ID. Pass 'all' to cancel every active monitor in this channel. "
1398
- "Use for: 'stop monitor m-abc123', 'stop all monitors', 'cancel the log watch'."
1397
+ "Delete a monitor by ID removes it from the list immediately. "
1398
+ "Works on any monitor regardless of status (active, done, or cancelled). "
1399
+ "For active monitors this also stops it from running. "
1400
+ "Pass 'all' to delete every monitor in this channel. "
1401
+ "Use for: 'delete monitor m-abc123', 'stop monitor m-abc123', 'delete all monitors', "
1402
+ "'clear monitors', 'cancel the log watch'."
1399
1403
  ),
1400
1404
  "input_schema": {
1401
1405
  "type": "object",
@@ -1411,8 +1415,9 @@ _TOOLS = [
1411
1415
  {
1412
1416
  "name": "list_monitors",
1413
1417
  "description": (
1414
- "List all active scheduled monitors. "
1415
- "Use for: 'what monitors are running?', 'show scheduled tasks', 'list active monitors'."
1418
+ "List all monitors — active ones plus completed/cancelled ones from the last 24 hours. "
1419
+ "Use for: 'what monitors are running?', 'show scheduled tasks', 'list monitors', "
1420
+ "'show recent monitors'. Each entry includes status (active/done/cancelled)."
1416
1421
  ),
1417
1422
  "input_schema": {"type": "object", "properties": {}},
1418
1423
  },
@@ -3589,32 +3594,33 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
3589
3594
  if not mon_id:
3590
3595
  return json.dumps({"error": "monitor_id is required"})
3591
3596
  if mon_id.lower() == "all":
3592
- count = store.cancel_all_monitors(channel=channel)
3593
- return json.dumps({"cancelled": count, "message": f"Cancelled {count} active monitor(s)."})
3594
- ok = store.cancel_monitor(mon_id)
3597
+ count = store.delete_all_monitors(channel=channel)
3598
+ return json.dumps({"deleted": count, "message": f"Deleted {count} monitor(s)."})
3599
+ ok = store.delete_monitor(mon_id)
3595
3600
  if ok:
3596
- return json.dumps({"status": "cancelled", "monitor_id": mon_id})
3597
- return json.dumps({"error": f"Monitor '{mon_id}' not found or already stopped."})
3601
+ return json.dumps({"status": "deleted", "monitor_id": mon_id})
3602
+ return json.dumps({"error": f"Monitor '{mon_id}' not found."})
3598
3603
 
3599
3604
  if name == "list_monitors":
3600
3605
  monitors = store.list_active_monitors()
3601
3606
  if not monitors:
3602
- return json.dumps({"monitors": [], "message": "No active monitors."})
3607
+ return json.dumps({"monitors": [], "message": "No monitors in the last 24 hours."})
3603
3608
  result = []
3604
3609
  for _m in monitors:
3605
3610
  _runs_left = None
3606
3611
  if _m.get("max_runs"):
3607
3612
  _runs_left = _m["max_runs"] - _m["runs_so_far"]
3608
3613
  result.append({
3609
- "id": _m["id"],
3610
- "name": _m.get("name") or "",
3611
- "status": _m["status"],
3612
- "interval": _format_duration(_m["interval_seconds"]),
3613
- "runs_so_far": _m["runs_so_far"],
3614
- "runs_left": _runs_left,
3615
- "next_run_at": _m.get("next_run_at") or "",
3616
- "stop_at": _m.get("stop_at") or "",
3617
- "steps": json.loads(_m.get("steps_json") or "[]"),
3614
+ "id": _m["id"],
3615
+ "name": _m.get("name") or "",
3616
+ "status": _m["status"],
3617
+ "interval": _format_duration(_m["interval_seconds"]),
3618
+ "runs_so_far": _m["runs_so_far"],
3619
+ "runs_left": _runs_left,
3620
+ "next_run_at": _m.get("next_run_at") or "",
3621
+ "stop_at": _m.get("stop_at") or "",
3622
+ "completed_at": _m.get("completed_at") or "",
3623
+ "steps": json.loads(_m.get("steps_json") or "[]"),
3618
3624
  })
3619
3625
  return json.dumps({"monitors": result})
3620
3626
 
@@ -450,8 +450,14 @@ class StateStore:
450
450
  "channel TEXT, "
451
451
  "user_id TEXT, "
452
452
  "status TEXT DEFAULT 'active', "
453
+ "completed_at TEXT, "
453
454
  "created_at TEXT)"
454
455
  )
456
+ # Migration: add completed_at to existing tables
457
+ try:
458
+ conn.execute("ALTER TABLE monitors ADD COLUMN completed_at TEXT")
459
+ except Exception:
460
+ pass
455
461
 
456
462
  def create_monitor(self, id: str, name: str, steps_json: str,
457
463
  interval_seconds: int, stop_at, max_runs,
@@ -486,10 +492,21 @@ class StateStore:
486
492
  return dict(row) if row else None
487
493
 
488
494
  def list_active_monitors(self) -> list[dict]:
495
+ from datetime import datetime, timezone, timedelta
496
+ now = datetime.now(timezone.utc)
497
+ cutoff = (now - timedelta(hours=24)).isoformat()
489
498
  with self._conn() as conn:
490
499
  self._ensure_monitors_table(conn)
500
+ # Auto-clean completed/done monitors older than 24 h
501
+ conn.execute(
502
+ "DELETE FROM monitors WHERE status != 'active' AND completed_at < ?",
503
+ (cutoff,),
504
+ )
491
505
  rows = conn.execute(
492
- "SELECT * FROM monitors WHERE status = 'active' ORDER BY created_at DESC"
506
+ "SELECT * FROM monitors "
507
+ "WHERE status = 'active' OR (status != 'active' AND completed_at >= ?) "
508
+ "ORDER BY created_at DESC",
509
+ (cutoff,),
493
510
  ).fetchall()
494
511
  return [dict(r) for r in rows]
495
512
 
@@ -505,7 +522,12 @@ class StateStore:
505
522
  with self._conn() as conn:
506
523
  self._ensure_monitors_table(conn)
507
524
  if done:
508
- conn.execute("DELETE FROM monitors WHERE id = ?", (id,))
525
+ # Keep done monitors visible for 24 h; user can delete explicitly via stop_monitor
526
+ conn.execute(
527
+ "UPDATE monitors SET runs_so_far = runs_so_far + 1, last_run_at = ?, "
528
+ "status = 'done', completed_at = ? WHERE id = ?",
529
+ (_now(), _now(), id),
530
+ )
509
531
  else:
510
532
  conn.execute(
511
533
  "UPDATE monitors SET runs_so_far = runs_so_far + 1, last_run_at = ?, "
@@ -513,26 +535,31 @@ class StateStore:
513
535
  (_now(), next_run_at, id),
514
536
  )
515
537
 
516
- def cancel_monitor(self, id: str) -> bool:
538
+ def delete_monitor(self, id: str) -> bool:
539
+ """Delete any monitor regardless of status (cancel + remove for active ones)."""
517
540
  with self._conn() as conn:
518
541
  self._ensure_monitors_table(conn)
519
- cur = conn.execute(
520
- "DELETE FROM monitors WHERE id = ? AND status = 'active'", (id,)
521
- )
542
+ cur = conn.execute("DELETE FROM monitors WHERE id = ?", (id,))
522
543
  return cur.rowcount > 0
523
544
 
524
- def cancel_all_monitors(self, channel: str = "") -> int:
545
+ def delete_all_monitors(self, channel: str = "") -> int:
546
+ """Delete all monitors in a channel (or globally if channel is empty)."""
525
547
  with self._conn() as conn:
526
548
  self._ensure_monitors_table(conn)
527
549
  if channel:
528
- cur = conn.execute(
529
- "DELETE FROM monitors WHERE status = 'active' AND channel = ?",
530
- (channel,),
531
- )
550
+ cur = conn.execute("DELETE FROM monitors WHERE channel = ?", (channel,))
532
551
  else:
533
- cur = conn.execute("DELETE FROM monitors WHERE status = 'active'")
552
+ cur = conn.execute("DELETE FROM monitors")
534
553
  return cur.rowcount
535
554
 
555
+ def cancel_monitor(self, id: str) -> bool:
556
+ """Alias for delete_monitor — kept for compatibility."""
557
+ return self.delete_monitor(id)
558
+
559
+ def cancel_all_monitors(self, channel: str = "") -> int:
560
+ """Alias for delete_all_monitors — kept for compatibility."""
561
+ return self.delete_all_monitors(channel)
562
+
536
563
  # ── Pending bot-message routing questions ─────────────────────────────────
537
564
 
538
565
  def _ensure_pending_routings_table(self, conn):