@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:
|
|
1
|
+
2026-03-21T20:51:14.848Z
|
package/.cairn/session.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"message": "Auto-checkpoint at 2026-03-21T20:
|
|
3
|
-
"checkpoint_at": "2026-03-21T20:
|
|
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
package/python/requirements.txt
CHANGED
|
@@ -21,25 +21,14 @@ CAIRN_BIN = "cairn"
|
|
|
21
21
|
|
|
22
22
|
def ensure_installed() -> bool:
|
|
23
23
|
try:
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
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
|
-
"""
|
|
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
|
|