@misterhuydo/sentinel 1.0.30 → 1.0.32

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-03-21T20:21:02.908Z
1
+ 2026-03-21T20:51:14.848Z
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-03-21T20:42:36.784Z",
3
- "checkpoint_at": "2026-03-21T20:42:36.786Z",
2
+ "message": "Auto-checkpoint at 2026-03-21T20:54:08.813Z",
3
+ "checkpoint_at": "2026-03-21T20:54:08.814Z",
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.30",
3
+ "version": "1.0.32",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -5,3 +5,4 @@ requests>=2.31
5
5
  jinja2>=3.1
6
6
  anthropic>=0.27
7
7
  slack-bolt>=1.18
8
+ aiohttp>=3.9
@@ -21,25 +21,14 @@ CAIRN_BIN = "cairn"
21
21
 
22
22
  def ensure_installed() -> bool:
23
23
  try:
24
- r = subprocess.run([CAIRN_BIN, "--version"], capture_output=True, text=True, timeout=10)
25
- if r.returncode == 0:
26
- logger.debug("Cairn version: %s", r.stdout.strip())
27
- return True
24
+ subprocess.run([CAIRN_BIN, "--version"], capture_output=True, text=True, timeout=10)
25
+ return True
28
26
  except FileNotFoundError:
29
27
  pass
30
- logger.error("Cairn not found. Run: npm install -g @misterhuydo/cairn-mcp")
28
+ logger.warning("cairn-mcp not found. Run: npm install -g @misterhuydo/cairn-mcp")
31
29
  return False
32
30
 
33
31
 
34
32
  def index_repo(repo: RepoConfig) -> bool:
35
- """Index a repo so Cairn context is available for Claude Code. Run at --init."""
36
- logger.info("Indexing %s with Cairn...", repo.repo_name)
37
- r = subprocess.run(
38
- [CAIRN_BIN, "index", "--path", repo.local_path],
39
- capture_output=True, text=True, timeout=300,
40
- )
41
- if r.returncode != 0:
42
- logger.error("Cairn index failed for %s:\n%s", repo.repo_name, r.stderr)
43
- return False
44
- logger.info("Cairn index complete for %s", repo.repo_name)
33
+ """No-op: Cairn indexes automatically via hooks when Claude Code runs."""
45
34
  return True
@@ -9,6 +9,8 @@ per turn — Claude may call multiple tools before replying.
9
9
  import json
10
10
  import logging
11
11
  import os
12
+ import re
13
+ import subprocess
12
14
  import uuid
13
15
  from datetime import datetime, timezone
14
16
  from pathlib import Path
@@ -263,6 +265,88 @@ def _run_tool(name: str, inputs: dict, cfg_loader, store) -> str:
263
265
  return json.dumps({"error": f"unknown tool: {name}"})
264
266
 
265
267
 
268
+ # ── CLI fallback (OAuth / no API key) ────────────────────────────────────────
269
+
270
+ _ACTION_RE = re.compile(r"^ACTION:\s*(\{.*\})", re.MULTILINE)
271
+
272
+
273
+ async def _handle_with_cli(
274
+ message: str,
275
+ history: list,
276
+ cfg_loader,
277
+ store,
278
+ ) -> tuple[str, bool]:
279
+ """Fallback: use `claude --print` for users without an Anthropic API key."""
280
+ status_json = _run_tool("get_status", {"hours": 24}, cfg_loader, store)
281
+ prs_json = _run_tool("list_pending_prs", {}, cfg_loader, store)
282
+
283
+ paused = Path("SENTINEL_PAUSE").exists()
284
+ repos = list(cfg_loader.repos.keys())
285
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
286
+
287
+ history_text = ""
288
+ for msg in history[-8:]:
289
+ role = msg["role"].upper()
290
+ content = msg["content"]
291
+ if isinstance(content, list):
292
+ content = " ".join(
293
+ (b.get("text", "") if isinstance(b, dict) else getattr(b, "text", ""))
294
+ for b in content
295
+ if (isinstance(b, dict) and b.get("type") == "text")
296
+ or (hasattr(b, "type") and b.type == "text")
297
+ )
298
+ history_text += f"\n{role}: {content}"
299
+
300
+ prompt = (
301
+ _SYSTEM
302
+ + f"\n\nCurrent time: {ts}"
303
+ + f"\nSentinel status: {'⏸ PAUSED' if paused else '▶ RUNNING'}"
304
+ + f"\nManaged repos: {', '.join(repos) if repos else '(none configured)'}"
305
+ + f"\n\nCurrent status (last 24 h):\n{status_json}"
306
+ + f"\n\nOpen PRs:\n{prs_json}"
307
+ + (f"\n\nConversation so far:{history_text}" if history_text else "")
308
+ + f"\n\nUSER: {message}"
309
+ + "\n\nIf you need to take an action, include a line like:\n"
310
+ + " ACTION: {\"action\": \"pause_sentinel\"}\n"
311
+ + " ACTION: {\"action\": \"resume_sentinel\"}\n"
312
+ + " ACTION: {\"action\": \"create_issue\", \"description\": \"...\", \"target_repo\": \"\"}\n"
313
+ + "End with [DONE] if the request is fully handled."
314
+ )
315
+
316
+ cfg = cfg_loader.sentinel
317
+ env = os.environ.copy()
318
+ if cfg.anthropic_api_key:
319
+ env["ANTHROPIC_API_KEY"] = cfg.anthropic_api_key
320
+
321
+ try:
322
+ result = subprocess.run(
323
+ [cfg.claude_code_bin, "--print", prompt],
324
+ capture_output=True, text=True, timeout=60, env=env,
325
+ )
326
+ output = (result.stdout or "").strip()
327
+ except Exception as e:
328
+ logger.error("Boss CLI call failed: %s", e)
329
+ return f":warning: Boss unavailable: {e}", True
330
+
331
+ for m in _ACTION_RE.finditer(output):
332
+ try:
333
+ action = json.loads(m.group(1))
334
+ name = action.pop("action", "")
335
+ if name:
336
+ result_str = _run_tool(name, action, cfg_loader, store)
337
+ logger.info("Boss CLI action: %s → %s", name, result_str[:80])
338
+ except Exception as e:
339
+ logger.warning("Boss action parse error: %s", e)
340
+
341
+ reply = _ACTION_RE.sub("", output).strip()
342
+ is_done = "[DONE]" in reply
343
+ reply = reply.replace("[DONE]", "").strip()
344
+
345
+ history.append({"role": "user", "content": message})
346
+ history.append({"role": "assistant", "content": reply})
347
+ return reply, is_done
348
+
349
+
266
350
  # ── Main entry point ──────────────────────────────────────────────────────────
267
351
 
268
352
  async def handle_message(
@@ -295,10 +379,7 @@ async def handle_message(
295
379
 
296
380
  api_key = cfg_loader.sentinel.anthropic_api_key or os.environ.get("ANTHROPIC_API_KEY", "")
297
381
  if not api_key:
298
- return (
299
- ":warning: `ANTHROPIC_API_KEY` not configured in `sentinel.properties`.",
300
- True,
301
- )
382
+ return await _handle_with_cli(message, history, cfg_loader, store)
302
383
 
303
384
  client = anthropic.Anthropic(api_key=api_key)
304
385