@misterhuydo/sentinel 1.0.66 → 1.0.68

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-22T15:53:29.003Z
1
+ 2026-03-22T16:24:09.877Z
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-03-22T16:04:55.232Z",
3
- "checkpoint_at": "2026-03-22T16:04:55.233Z",
2
+ "message": "Auto-checkpoint at 2026-03-22T16:22:20.785Z",
3
+ "checkpoint_at": "2026-03-22T16:22:20.786Z",
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.66",
3
+ "version": "1.0.68",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -100,6 +100,17 @@ What you can do (tools available):
100
100
  process. Safe to run at any time — no restart if already up to date.
101
101
  e.g. "upgrade sentinel", "update sentinel", "upgrade yourself"
102
102
 
103
+ 19. ask_codebase — Ask any natural-language question about a managed repo's codebase.
104
+ Claude Code answers using its full knowledge of the code.
105
+ e.g. "what does the 1881 backend do?", "find PIN validation in elprint",
106
+ "any TODOs in cairn?", "are there security issues in elprint-sales?"
107
+
108
+ 20. restart_project — Stop and restart a specific project instance (stop.sh + start.sh).
109
+ e.g. "restart 1881", "reboot elprint", "restart the cairn project"
110
+
111
+ 21. tail_log — Fetch the last N lines of a log source live, without a grep filter.
112
+ e.g. "show recent SSOLWA logs", "tail STS", "last 200 lines from 1881 logs"
113
+
103
114
  When someone asks what you can do, what you support, what your capabilities are, or how you can help,
104
115
  reply with a short summary grouped by category:
105
116
 
@@ -111,6 +122,10 @@ reply with a short summary grouped by category:
111
122
  *Log management*
112
123
  • `fetch_logs` — pull fresh logs from servers right now — "fetch logs for SSOLWA"
113
124
  • `search_logs` — live SSH grep on production servers — "search logs for illegal PIN in 1881"
125
+ • `tail_log` — last N lines of a log source, no filter — "show recent SSOLWA logs"
126
+
127
+ *Codebase questions*
128
+ • `ask_codebase` — any question about a repo's code — "what does 1881 do?", "find PIN validation", "any TODOs?", "security issues?"
114
129
 
115
130
  *Fix management*
116
131
  • `get_fix_details` — full details of a specific fix — "show fix abc123"
@@ -131,6 +146,9 @@ reply with a short summary grouped by category:
131
146
  • `unwatch_bot` — stop monitoring a bot — "stop watching @errorbot"
132
147
  • `list_watched_bots` — show all bots currently being monitored — "which bots are you watching?"
133
148
 
149
+ *Project control*
150
+ • `restart_project` — stop + restart a specific project — "restart 1881"
151
+
134
152
  *Self-management*
135
153
  • `upgrade_sentinel` — git pull + pip install + restart — "upgrade sentinel", "update yourself"
136
154
 
@@ -433,6 +451,72 @@ _TOOLS = [
433
451
  ),
434
452
  "input_schema": {"type": "object", "properties": {}},
435
453
  },
454
+ {
455
+ "name": "ask_codebase",
456
+ "description": (
457
+ "Ask any natural-language question about a managed repo's codebase. "
458
+ "Claude Code answers using its full codebase knowledge — no need to specify how. "
459
+ "Use for any codebase question: 'what does the 1881 backend do?', "
460
+ "'find PIN validation code in elprint', 'are there TODOs in cairn?', "
461
+ "'what classes handle auth?', 'any security issues in elprint-sales?', "
462
+ "'summarize the architecture of 1881', 'show me the dependency graph'."
463
+ ),
464
+ "input_schema": {
465
+ "type": "object",
466
+ "properties": {
467
+ "repo": {
468
+ "type": "string",
469
+ "description": "Repo name (partial match, e.g. '1881', 'elprint-sales', 'cairn')",
470
+ },
471
+ "question": {
472
+ "type": "string",
473
+ "description": "Natural language question about the codebase",
474
+ },
475
+ },
476
+ "required": ["repo", "question"],
477
+ },
478
+ },
479
+ {
480
+ "name": "restart_project",
481
+ "description": (
482
+ "Stop and restart a specific Sentinel project instance (runs stop.sh then start.sh). "
483
+ "Use when: 'restart 1881', 'restart elprint', 'reboot the cairn project'. "
484
+ "Safer than restarting all projects at once."
485
+ ),
486
+ "input_schema": {
487
+ "type": "object",
488
+ "properties": {
489
+ "project": {
490
+ "type": "string",
491
+ "description": "Project short name or dir name (e.g. '1881', 'elprint')",
492
+ },
493
+ },
494
+ "required": ["project"],
495
+ },
496
+ },
497
+ {
498
+ "name": "tail_log",
499
+ "description": (
500
+ "Fetch the last N lines of a log source's live production logs without any grep filter. "
501
+ "Use when: 'show me recent SSOLWA logs', 'tail STS', 'what's happening in 1881 logs right now', "
502
+ "'show last 100 lines from SSOLWA'. Different from search_logs — no pattern required."
503
+ ),
504
+ "input_schema": {
505
+ "type": "object",
506
+ "properties": {
507
+ "source": {
508
+ "type": "string",
509
+ "description": "Log source name (partial match against log-config filenames, e.g. 'SSOLWA', 'STS')",
510
+ },
511
+ "lines": {
512
+ "type": "integer",
513
+ "description": "Number of recent lines to fetch (default 100)",
514
+ "default": 100,
515
+ },
516
+ },
517
+ "required": ["source"],
518
+ },
519
+ },
436
520
  ]
437
521
 
438
522
 
@@ -949,6 +1033,112 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
949
1033
  "note": "Upgrade started — pulling latest version via npm and restarting. Give me ~30 seconds then I'll be back.",
950
1034
  })
951
1035
 
1036
+ if name == "ask_codebase":
1037
+ repo_name = inputs.get("repo", "").lower()
1038
+ question = inputs.get("question", "")
1039
+
1040
+ repo_cfg = next(
1041
+ (r for rn, r in cfg_loader.repos.items() if repo_name in rn.lower()),
1042
+ None,
1043
+ )
1044
+ if not repo_cfg:
1045
+ return json.dumps({"error": f"Repo '{repo_name}' not found", "available": list(cfg_loader.repos.keys())})
1046
+
1047
+ local_path = Path(repo_cfg.local_path)
1048
+ if not local_path.exists():
1049
+ return json.dumps({"error": f"Repo not cloned yet at {local_path}. Run pull_repo first."})
1050
+
1051
+ prompt = (
1052
+ f"You are a code analyst. Answer the following question about the codebase at: {local_path}\n\n"
1053
+ f"Question: {question}\n\n"
1054
+ f"Use whatever tools you need to answer accurately. Be concise and direct. Plain text only."
1055
+ )
1056
+
1057
+ cfg = cfg_loader.sentinel
1058
+ env = os.environ.copy()
1059
+ if cfg.anthropic_api_key:
1060
+ env["ANTHROPIC_API_KEY"] = cfg.anthropic_api_key
1061
+
1062
+ try:
1063
+ r = subprocess.run(
1064
+ [cfg.claude_code_bin, "--dangerously-skip-permissions", "--print", prompt],
1065
+ capture_output=True, text=True, timeout=180, env=env,
1066
+ cwd=str(local_path),
1067
+ )
1068
+ output = (r.stdout or "").strip()
1069
+ if r.returncode != 0 and not output:
1070
+ return json.dumps({
1071
+ "error": f"`claude --print` failed (rc={r.returncode})",
1072
+ "stderr": (r.stderr or "").strip()[:300],
1073
+ })
1074
+ logger.info("Boss ask_codebase %s rc=%d len=%d", repo_cfg.repo_name, r.returncode, len(output))
1075
+ return json.dumps({"repo": repo_cfg.repo_name, "answer": output[:4000]})
1076
+ except subprocess.TimeoutExpired:
1077
+ return json.dumps({"error": "Codebase query timed out after 180s"})
1078
+ except Exception as e:
1079
+ return json.dumps({"error": str(e)})
1080
+
1081
+ if name == "restart_project":
1082
+ project_arg = inputs.get("project", "").lower()
1083
+ dirs = _find_project_dirs(project_arg)
1084
+ if not dirs:
1085
+ return json.dumps({"error": f"No project found matching '{project_arg}'"})
1086
+ results = []
1087
+ for d in dirs:
1088
+ stop_sh = d / "stop.sh"
1089
+ start_sh = d / "start.sh"
1090
+ if not stop_sh.exists() or not start_sh.exists():
1091
+ results.append({"project": d.name, "status": "error", "detail": "stop.sh or start.sh not found"})
1092
+ continue
1093
+ try:
1094
+ subprocess.run(["bash", str(stop_sh)], cwd=str(d), timeout=30)
1095
+ subprocess.run(["bash", str(start_sh)], cwd=str(d), timeout=30)
1096
+ results.append({"project": d.name, "status": "restarted"})
1097
+ logger.info("Boss: restarted project %s", d.name)
1098
+ except Exception as e:
1099
+ results.append({"project": d.name, "status": "error", "detail": str(e)})
1100
+ return json.dumps({"results": results})
1101
+
1102
+ if name == "tail_log":
1103
+ source = inputs.get("source", "").lower()
1104
+ lines = int(inputs.get("lines", 100))
1105
+ script = Path(__file__).resolve().parent.parent / "scripts" / "fetch_log.sh"
1106
+ log_cfg_dir = Path("config") / "log-configs"
1107
+
1108
+ if not script.exists():
1109
+ return json.dumps({"error": "fetch_log.sh not found"})
1110
+ if not log_cfg_dir.exists():
1111
+ return json.dumps({"error": "config/log-configs/ not found"})
1112
+
1113
+ props_files = sorted(log_cfg_dir.glob("*.properties"))
1114
+ if source:
1115
+ props_files = [p for p in props_files if source in p.stem.lower()]
1116
+ if not props_files:
1117
+ return json.dumps({"error": f"No log-config found matching '{source}'"})
1118
+
1119
+ results = []
1120
+ for props in props_files:
1121
+ env = os.environ.copy()
1122
+ env["TAIL"] = str(lines)
1123
+ env["GREP_FILTER"] = "" # no filter — show everything
1124
+ try:
1125
+ r = subprocess.run(
1126
+ ["bash", str(script), str(props)],
1127
+ capture_output=True, text=True, timeout=60, env=env,
1128
+ )
1129
+ tail_lines = (r.stdout or "").strip().splitlines()[-lines:]
1130
+ results.append({
1131
+ "source": props.stem,
1132
+ "lines": len(tail_lines),
1133
+ "content": "\n".join(tail_lines),
1134
+ })
1135
+ logger.info("Boss tail_log %s rc=%d lines=%d", props.stem, r.returncode, len(tail_lines))
1136
+ except subprocess.TimeoutExpired:
1137
+ results.append({"source": props.stem, "error": "timed out"})
1138
+ except Exception as e:
1139
+ results.append({"source": props.stem, "error": str(e)})
1140
+ return json.dumps({"results": results})
1141
+
952
1142
  return json.dumps({"error": f"unknown tool: {name}"})
953
1143
 
954
1144