@misterhuydo/sentinel 1.0.32 → 1.0.34

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-21T20:54:08.813Z",
3
- "checkpoint_at": "2026-03-21T20:54:08.814Z",
2
+ "message": "Auto-checkpoint at 2026-03-21T21:16:28.480Z",
3
+ "checkpoint_at": "2026-03-21T21:16:28.481Z",
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.32",
3
+ "version": "1.0.34",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -571,7 +571,16 @@ async def run_loop(cfg_loader: ConfigLoader, store: StateStore):
571
571
  await poll_cycle(cfg_loader, store)
572
572
  except Exception as e:
573
573
  logger.exception("Unhandled error in poll cycle: %s", e)
574
- await asyncio.sleep(interval)
574
+ # Sleep in 5s ticks so SENTINEL_POLL_NOW triggers immediately
575
+ elapsed = 0
576
+ while elapsed < interval:
577
+ await asyncio.sleep(5)
578
+ elapsed += 5
579
+ poll_now = Path("SENTINEL_POLL_NOW")
580
+ if poll_now.exists():
581
+ poll_now.unlink(missing_ok=True)
582
+ logger.info("Immediate poll triggered by Sentinel Boss")
583
+ break
575
584
 
576
585
 
577
586
  def main():
@@ -122,6 +122,77 @@ _TOOLS = [
122
122
  ),
123
123
  "input_schema": {"type": "object", "properties": {}},
124
124
  },
125
+ {
126
+ "name": "search_logs",
127
+ "description": (
128
+ "Search fetched log files for a pattern. Use for: 'find activity from appid=X', "
129
+ "'search logs for Y', 'show me log entries containing Z', 'what did user X do?'. "
130
+ "Supports any regex or plain text pattern."
131
+ ),
132
+ "input_schema": {
133
+ "type": "object",
134
+ "properties": {
135
+ "query": {
136
+ "type": "string",
137
+ "description": "Regex or plain text to search for",
138
+ },
139
+ "source": {
140
+ "type": "string",
141
+ "description": "Optional: limit to a specific log source name (partial match)",
142
+ },
143
+ "max_matches": {
144
+ "type": "integer",
145
+ "description": "Max matching lines to return per file (default 20)",
146
+ "default": 20,
147
+ },
148
+ },
149
+ "required": ["query"],
150
+ },
151
+ },
152
+ {
153
+ "name": "trigger_poll",
154
+ "description": (
155
+ "Trigger an immediate log-fetch and error-detection cycle without waiting "
156
+ "for the next scheduled interval. Use when: 'check now', 'run now', "
157
+ "'poll immediately', 'don't wait'."
158
+ ),
159
+ "input_schema": {"type": "object", "properties": {}},
160
+ },
161
+ {
162
+ "name": "get_repo_status",
163
+ "description": (
164
+ "Per-repository breakdown of errors detected and fixes applied. "
165
+ "Use for: 'how is repo X doing?', 'which repo has the most issues?', "
166
+ "'break down by repo'."
167
+ ),
168
+ "input_schema": {
169
+ "type": "object",
170
+ "properties": {
171
+ "hours": {
172
+ "type": "integer",
173
+ "description": "Look-back window in hours (default 24)",
174
+ "default": 24,
175
+ },
176
+ },
177
+ },
178
+ },
179
+ {
180
+ "name": "list_recent_commits",
181
+ "description": (
182
+ "List recent commits made by Sentinel across all managed repos. "
183
+ "Use for: 'what did Sentinel commit?', 'show recent auto-fixes', 'what was changed?'."
184
+ ),
185
+ "input_schema": {
186
+ "type": "object",
187
+ "properties": {
188
+ "limit": {
189
+ "type": "integer",
190
+ "description": "Max commits per repo (default 5)",
191
+ "default": 5,
192
+ },
193
+ },
194
+ },
195
+ },
125
196
  ]
126
197
 
127
198
 
@@ -262,6 +333,76 @@ def _run_tool(name: str, inputs: dict, cfg_loader, store) -> str:
262
333
  "workspace_projects": other_projects,
263
334
  })
264
335
 
336
+ if name == "search_logs":
337
+ query = inputs.get("query", "")
338
+ source = inputs.get("source", "").lower()
339
+ max_matches = int(inputs.get("max_matches", 20))
340
+ fetched_dir = Path("workspace/fetched")
341
+ if not fetched_dir.exists():
342
+ return json.dumps({"error": "No fetched logs found — run a poll first"})
343
+ try:
344
+ pattern = re.compile(query, re.IGNORECASE)
345
+ except re.error as e:
346
+ return json.dumps({"error": f"Invalid regex: {e}"})
347
+ results = []
348
+ for log_file in sorted(fetched_dir.glob("*.log")):
349
+ if source and source not in log_file.name.lower():
350
+ continue
351
+ try:
352
+ lines = log_file.read_text(encoding="utf-8", errors="ignore").splitlines()
353
+ matches = [
354
+ {"line": i + 1, "text": line[:300]}
355
+ for i, line in enumerate(lines)
356
+ if pattern.search(line)
357
+ ][:max_matches]
358
+ if matches:
359
+ results.append({"file": log_file.name, "matches": matches})
360
+ except Exception:
361
+ pass
362
+ total = sum(len(r["matches"]) for r in results)
363
+ return json.dumps({
364
+ "query": query,
365
+ "total_matches": total,
366
+ "files_searched": len(list(fetched_dir.glob("*.log"))),
367
+ "results": results,
368
+ })
369
+
370
+ if name == "trigger_poll":
371
+ Path("SENTINEL_POLL_NOW").touch()
372
+ logger.info("Boss: immediate poll requested")
373
+ return json.dumps({"status": "triggered", "note": "Sentinel will run a poll cycle within seconds"})
374
+
375
+ if name == "get_repo_status":
376
+ hours = int(inputs.get("hours", 24))
377
+ fixes = store.get_recent_fixes(hours)
378
+ errors = store.get_recent_errors(hours)
379
+ by_repo: dict = {}
380
+ for fix in fixes:
381
+ repo = fix.get("repo_name", "unknown")
382
+ s = by_repo.setdefault(repo, {"applied": 0, "pending": 0, "failed": 0, "skipped": 0})
383
+ key = fix.get("status", "failed")
384
+ s[key] = s.get(key, 0) + 1
385
+ return json.dumps({"window_hours": hours, "total_errors": len(errors), "by_repo": by_repo})
386
+
387
+ if name == "list_recent_commits":
388
+ limit = int(inputs.get("limit", 5))
389
+ results = []
390
+ for repo_name, repo in cfg_loader.repos.items():
391
+ local = Path(repo.local_path)
392
+ if not local.exists():
393
+ continue
394
+ try:
395
+ r = subprocess.run(
396
+ ["git", "log", "--oneline", "--grep=sentinel", "-n", str(limit)],
397
+ cwd=str(local), capture_output=True, text=True, timeout=10,
398
+ )
399
+ commits = r.stdout.strip().splitlines()
400
+ if commits:
401
+ results.append({"repo": repo_name, "commits": commits})
402
+ except Exception:
403
+ pass
404
+ return json.dumps({"sentinel_commits": results})
405
+
265
406
  return json.dumps({"error": f"unknown tool: {name}"})
266
407
 
267
408
 
@@ -280,6 +421,16 @@ async def _handle_with_cli(
280
421
  status_json = _run_tool("get_status", {"hours": 24}, cfg_loader, store)
281
422
  prs_json = _run_tool("list_pending_prs", {}, cfg_loader, store)
282
423
 
424
+ # Pre-fetch log search if the message is a search request.
425
+ # Use quoted strings as the query, or fall back to the full message.
426
+ # Never hardcode field names — the query is whatever the user said.
427
+ search_json = ""
428
+ _search_kws = ("search", "find", "look for", "show me log", "grep", "entries for")
429
+ if any(kw in message.lower() for kw in _search_kws):
430
+ quoted = re.findall(r'"([^"]+)"', message)
431
+ query = quoted[0] if quoted else message
432
+ search_json = _run_tool("search_logs", {"query": query}, cfg_loader, store)
433
+
283
434
  paused = Path("SENTINEL_PAUSE").exists()
284
435
  repos = list(cfg_loader.repos.keys())
285
436
  ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
@@ -304,12 +455,15 @@ async def _handle_with_cli(
304
455
  + f"\nManaged repos: {', '.join(repos) if repos else '(none configured)'}"
305
456
  + f"\n\nCurrent status (last 24 h):\n{status_json}"
306
457
  + f"\n\nOpen PRs:\n{prs_json}"
458
+ + (f"\n\nLog search results:\n{search_json}" if search_json else "")
307
459
  + (f"\n\nConversation so far:{history_text}" if history_text else "")
308
460
  + f"\n\nUSER: {message}"
309
461
  + "\n\nIf you need to take an action, include a line like:\n"
310
462
  + " ACTION: {\"action\": \"pause_sentinel\"}\n"
311
463
  + " ACTION: {\"action\": \"resume_sentinel\"}\n"
464
+ + " ACTION: {\"action\": \"trigger_poll\"}\n"
312
465
  + " ACTION: {\"action\": \"create_issue\", \"description\": \"...\", \"target_repo\": \"\"}\n"
466
+ + " ACTION: {\"action\": \"search_logs\", \"query\": \"<whatever the user asked to find>\"}\n"
313
467
  + "End with [DONE] if the request is fully handled."
314
468
  )
315
469