@misterhuydo/sentinel 1.0.58 → 1.0.60

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-22T14:06:29.625Z
1
+ 2026-03-22T14:37:46.791Z
@@ -1 +1,8 @@
1
- {}
1
+ {
2
+ "J:\\Projects\\Sentinel\\cli\\lib\\upgrade.js": {
3
+ "tempPath": "J:\\Projects\\Sentinel\\cli\\.cairn\\views\\fb78ac_upgrade.js",
4
+ "state": "compressed",
5
+ "minifiedAt": 1774129312316.9353,
6
+ "readCount": 1
7
+ }
8
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-03-22T14:28:09.441Z",
3
- "checkpoint_at": "2026-03-22T14:28:09.442Z",
2
+ "message": "Auto-checkpoint at 2026-03-22T14:43:35.624Z",
3
+ "checkpoint_at": "2026-03-22T14:43:35.625Z",
4
4
  "active_files": [],
5
5
  "notes": [],
6
6
  "mtime_snapshot": {}
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const { execSync, spawnSync } = require('child_process');
6
+ const chalk = require('chalk');
7
+ const ok = msg => console.log(chalk.green(' ✔'), msg);
8
+ const info = msg => console.log(chalk.cyan(' →'), msg);
9
+ const warn = msg => console.log(chalk.yellow(' ⚠'), msg);
10
+ module.exports = async function upgrade() {
11
+ const { version: current } = require('../package.json');
12
+ const defaultWorkspace = path.join(os.homedir(), 'sentinel');
13
+ const codeDir = path.join(defaultWorkspace, 'code');
14
+ if (!fs.existsSync(codeDir)) {
15
+ console.error(chalk.red(' ✖ Sentinel code directory not found at ' + codeDir));
16
+ console.error(' Run: sentinel init');
17
+ process.exit(1);
18
+ }
19
+ info(`Current version: ${current}`);
20
+ info('Installing latest @misterhuydo/sentinel...');
21
+ const install = spawnSync('npm', ['install', '-g', '@misterhuydo/sentinel@latest'], { stdio: 'inherit' });
22
+ if (install.status !== 0) {
23
+ console.error(chalk.red(' ✖ npm install failed'));
24
+ process.exit(1);
25
+ }
26
+ const npmRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
27
+ const pkgDir = path.join(npmRoot, '@misterhuydo', 'sentinel');
28
+ const src = path.join(pkgDir, 'python');
29
+ if (!fs.existsSync(src)) {
30
+ console.error(chalk.red(' ✖ Bundled Python source not found in installed package'));
31
+ process.exit(1);
32
+ }
33
+ info('Deploying Python source...');
34
+ fs.copySync(src, codeDir, { overwrite: true });
35
+ const scriptsDir = path.join(codeDir, 'scripts');
36
+ if (fs.existsSync(scriptsDir)) {
37
+ const shFiles = fs.readdirSync(scriptsDir)
38
+ .filter(f => f.endsWith('.sh'))
39
+ .map(f => path.join(scriptsDir, f));
40
+ if (shFiles.length) spawnSync('chmod', ['+x', ...shFiles], { stdio: 'inherit' });
41
+ }
42
+ ok('Python source updated');
43
+ const { version: latest } = require(path.join(pkgDir, 'package.json'));
44
+ ok(`Upgraded: ${current} → ${latest}`);
45
+ const startAll = path.join(defaultWorkspace, 'startAll.sh');
46
+ const stopAll = path.join(defaultWorkspace, 'stopAll.sh');
47
+ if (fs.existsSync(stopAll) && fs.existsSync(startAll)) {
48
+ info('Restarting Sentinel...');
49
+ spawnSync('bash', [stopAll], { stdio: 'inherit' });
50
+ spawnSync('bash', [startAll], { stdio: 'inherit' });
51
+ ok('Sentinel restarted');
52
+ } else {
53
+ warn('No startAll.sh found — restart manually');
54
+ }
55
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.0.58",
3
+ "version": "1.0.60",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -875,70 +875,36 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
875
875
 
876
876
  if name == "upgrade_sentinel":
877
877
  import threading
878
- import signal as _sig
879
-
880
- code_dir = Path(__file__).resolve().parent.parent # sentinel repo root
881
- project_dir = Path(".").resolve()
882
- steps: list[dict] = []
883
-
884
- # Step 1: git pull the sentinel agent code
885
- pull = _git_pull(code_dir)
886
- steps.append({"step": "git_pull", **pull})
887
- already_latest = "already up to date" in pull.get("detail", "").lower()
888
-
889
- if pull["status"] == "error":
890
- return json.dumps({"status": "error", "steps": steps,
891
- "note": "git pull failed — check network / SSH key"})
892
-
893
- # Step 2: pip install (update Python deps)
894
- venv_pip = code_dir / ".venv" / "bin" / "pip"
895
- pip_cmd = str(venv_pip) if venv_pip.exists() else "pip3"
896
- req_file = code_dir / "requirements.txt"
897
- if req_file.exists():
898
- try:
899
- r = subprocess.run(
900
- [pip_cmd, "install", "-q", "-r", str(req_file)],
901
- capture_output=True, text=True, timeout=120,
902
- )
903
- steps.append({
904
- "step": "pip_install",
905
- "status": "ok" if r.returncode == 0 else "warn",
906
- "detail": (r.stderr or "").strip()[:200],
907
- })
908
- except Exception as e:
909
- steps.append({"step": "pip_install", "status": "warn", "detail": str(e)})
910
878
 
911
- if already_latest:
879
+ # Sentinel is installed via npm — use `sentinel upgrade` which handles
880
+ # npm install + Python bundle copy + restart via stopAll/startAll.
881
+ # Run it in the background after a short delay so the Slack reply is
882
+ # sent before the process is replaced.
883
+ try:
884
+ r = subprocess.run(
885
+ ["sentinel", "--version"],
886
+ capture_output=True, text=True, timeout=10,
887
+ )
888
+ sentinel_bin_ok = r.returncode == 0
889
+ except Exception:
890
+ sentinel_bin_ok = False
891
+
892
+ if not sentinel_bin_ok:
912
893
  return json.dumps({
913
- "status": "already_latest",
914
- "steps": steps,
915
- "note": "Sentinel is already up to date. No restart needed.",
894
+ "status": "error",
895
+ "note": "`sentinel` CLI not found. Run: npm install -g @misterhuydo/sentinel",
916
896
  })
917
897
 
918
- # Step 3: restart — schedule after response is sent
919
- stop_sh = project_dir / "stop.sh"
920
- start_sh = project_dir / "start.sh"
921
- if stop_sh.exists() and start_sh.exists():
922
- def _restart_scripts():
923
- import time; time.sleep(10)
924
- subprocess.Popen(
925
- f"bash {stop_sh} && sleep 2 && bash {start_sh}",
926
- shell=True,
927
- )
928
- threading.Thread(target=_restart_scripts, daemon=True).start()
929
- restart_method = "stop.sh + start.sh"
930
- else:
931
- # SIGTERM self — systemd (Restart=always) will bring it back up
932
- # 10s delay gives Claude time to generate + post the reply before we die
933
- threading.Timer(10.0, lambda: os.kill(os.getpid(), _sig.SIGTERM)).start()
934
- restart_method = "SIGTERM → systemd restart"
898
+ def _do_upgrade():
899
+ import time
900
+ time.sleep(10) # give Slack time to post the reply
901
+ subprocess.Popen(["sentinel", "upgrade"], close_fds=True)
935
902
 
936
- steps.append({"step": "restart", "status": "scheduled", "method": restart_method})
937
- logger.info("Boss: upgrade_sentinel complete, restarting via %s", restart_method)
903
+ threading.Thread(target=_do_upgrade, daemon=True).start()
904
+ logger.info("Boss: upgrade_sentinel scheduled via `sentinel upgrade`")
938
905
  return json.dumps({
939
906
  "status": "ok",
940
- "steps": steps,
941
- "note": "Upgrade complete. Sentinel is restarting — give it a few seconds then I'll be back.",
907
+ "note": "Upgrade started — pulling latest version via npm and restarting. Give me ~30 seconds then I'll be back.",
942
908
  })
943
909
 
944
910
  return json.dumps({"error": f"unknown tool: {name}"})
@@ -955,6 +921,7 @@ async def _handle_with_cli(
955
921
  cfg_loader,
956
922
  store,
957
923
  slack_client=None,
924
+ user_name: str = "",
958
925
  ) -> tuple[str, bool]:
959
926
  """Fallback: use `claude --print` for users without an Anthropic API key."""
960
927
  status_json = await _run_tool("get_status", {"hours": 24}, cfg_loader, store)
@@ -989,6 +956,7 @@ async def _handle_with_cli(
989
956
 
990
957
  prompt = (
991
958
  _SYSTEM
959
+ + (f"\nYou are speaking with: {user_name}" if user_name else "")
992
960
  + f"\n\nCurrent time: {ts}"
993
961
  + f"\nSentinel status: {'⏸ PAUSED' if paused else '▶ RUNNING'}"
994
962
  + f"\nManaged repos: {', '.join(repos) if repos else '(none configured)'}"
@@ -1050,6 +1018,7 @@ async def handle_message(
1050
1018
  cfg_loader,
1051
1019
  store,
1052
1020
  slack_client=None,
1021
+ user_name: str = "",
1053
1022
  ) -> tuple[str, bool]:
1054
1023
  """
1055
1024
  Process one user message through the Sentinel Boss (Claude with tool use).
@@ -1075,7 +1044,7 @@ async def handle_message(
1075
1044
 
1076
1045
  api_key = cfg_loader.sentinel.anthropic_api_key or os.environ.get("ANTHROPIC_API_KEY", "")
1077
1046
  if not api_key:
1078
- return await _handle_with_cli(message, history, cfg_loader, store, slack_client=slack_client)
1047
+ return await _handle_with_cli(message, history, cfg_loader, store, slack_client=slack_client, user_name=user_name)
1079
1048
 
1080
1049
  client = anthropic.Anthropic(api_key=api_key)
1081
1050
 
@@ -1086,6 +1055,7 @@ async def handle_message(
1086
1055
  known_projects = [_read_project_name(d) for d in _find_project_dirs()]
1087
1056
  system = (
1088
1057
  _SYSTEM
1058
+ + (f"\nYou are speaking with: {user_name}" if user_name else "")
1089
1059
  + f"\n\nCurrent time: {ts}"
1090
1060
  + f"\nSentinel status: {'⏸ PAUSED' if paused else '▶ RUNNING'}"
1091
1061
  + f"\nManaged repos: {', '.join(repos) if repos else '(none configured)'}"
@@ -330,6 +330,7 @@ async def _run_turn(session: _Session, message: str, client, cfg_loader, store)
330
330
  reply, is_done = await handle_message(
331
331
  message, session.history, cfg_loader, store,
332
332
  slack_client=client,
333
+ user_name=session.user_name,
333
334
  )
334
335
  except Exception as e:
335
336
  logger.exception("Sentinel Boss error: %s", e)