@misterhuydo/sentinel 1.0.77 → 1.0.83

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/lib/upgrade.js CHANGED
@@ -1,3 +1,41 @@
1
+
2
+ function ensureClaudePermissions() {
3
+ const settingsPath = require('path').join(require('os').homedir(), '.claude', 'settings.json');
4
+ const required = ['Read(**)', 'Write(**)', 'Edit(**)', 'Bash(**)'];
5
+ let settings = {};
6
+ try {
7
+ if (fs.existsSync(settingsPath)) {
8
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
9
+ }
10
+ } catch (e) {
11
+ warn('Could not read ' + settingsPath + ': ' + e.message);
12
+ return;
13
+ }
14
+ if (!settings.permissions) settings.permissions = {};
15
+ if (!Array.isArray(settings.permissions.allow)) settings.permissions.allow = [];
16
+ const existing = new Set(settings.permissions.allow);
17
+ const added = [];
18
+ for (const perm of required) {
19
+ if (!existing.has(perm)) {
20
+ settings.permissions.allow.push(perm);
21
+ added.push(perm);
22
+ }
23
+ }
24
+ if (added.length === 0) {
25
+ ok('Claude Code permissions already configured');
26
+ return;
27
+ }
28
+ try {
29
+ const path = require('path');
30
+ fs.ensureDirSync(path.dirname(settingsPath));
31
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '
32
+ ');
33
+ ok('Claude Code permissions patched: ' + added.join(', '));
34
+ } catch (e) {
35
+ warn('Could not write ' + settingsPath + ': ' + e.message);
36
+ }
37
+ }
38
+
1
39
  'use strict';
2
40
 
3
41
  const fs = require('fs-extra');
@@ -67,6 +105,8 @@ module.exports = async function upgrade() {
67
105
  if (shFiles.length) spawnSync('chmod', ['+x', ...shFiles], { stdio: 'inherit' });
68
106
  }
69
107
  ok('Python source updated');
108
+ info('Patching Claude Code permissions…');
109
+ ensureClaudePermissions();
70
110
 
71
111
  // Print new version
72
112
  const { version: latest } = require(path.join(pkgDir, 'package.json'));
package/package.json CHANGED
@@ -1,21 +1,21 @@
1
- {
2
- "name": "@misterhuydo/sentinel",
3
- "version": "1.0.77",
4
- "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
- "bin": {
6
- "sentinel": "./bin/sentinel.js"
7
- },
8
- "scripts": {
9
- "prepublishOnly": "node scripts/bundle.js"
10
- },
11
- "dependencies": {
12
- "chalk": "^4.1.2",
13
- "fs-extra": "^11.2.0",
14
- "prompts": "^2.4.2"
15
- },
16
- "engines": {
17
- "node": ">=16"
18
- },
19
- "author": "misterhuydo",
20
- "license": "MIT"
21
- }
1
+ {
2
+ "name": "@misterhuydo/sentinel",
3
+ "version": "1.0.83",
4
+ "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
+ "bin": {
6
+ "sentinel": "./bin/sentinel.js"
7
+ },
8
+ "scripts": {
9
+ "prepublishOnly": "node scripts/bundle.js"
10
+ },
11
+ "dependencies": {
12
+ "chalk": "^4.1.2",
13
+ "fs-extra": "^11.2.0",
14
+ "prompts": "^2.4.2"
15
+ },
16
+ "engines": {
17
+ "node": ">=16"
18
+ },
19
+ "author": "misterhuydo",
20
+ "license": "MIT"
21
+ }
@@ -0,0 +1,200 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+
4
+ // ── fix_engine.py ─────────────────────────────────────────────────────────────
5
+ {
6
+ const f = 'sentinel/fix_engine.py';
7
+ let c = fs.readFileSync(f, 'utf8').replace(/\r\n/g, '\n');
8
+
9
+ // 1. Add notify import
10
+ const OLD_IMPORT = 'from .config_loader import RepoConfig, SentinelConfig\nfrom .log_parser import ErrorEvent';
11
+ const NEW_IMPORT = 'from .config_loader import RepoConfig, SentinelConfig\nfrom .log_parser import ErrorEvent\nfrom .notify import alert_if_rate_limited, slack_alert';
12
+ if (!c.includes(OLD_IMPORT)) { console.error('fix_engine import not found'); process.exit(1); }
13
+ c = c.replace(OLD_IMPORT, NEW_IMPORT);
14
+
15
+ // 2. Only inject ANTHROPIC_API_KEY when claude_pro_for_tasks is False
16
+ // (so when both are configured, heavy tasks use Claude Pro OAuth)
17
+ const OLD_ENV = ` import os as _os
18
+ env = _os.environ.copy()
19
+ if cfg.anthropic_api_key:
20
+ env["ANTHROPIC_API_KEY"] = cfg.anthropic_api_key`;
21
+ const NEW_ENV = ` import os as _os
22
+ env = _os.environ.copy()
23
+ # Inject API key only when Claude Pro is NOT preferred for tasks
24
+ # (when claude_pro_for_tasks=True and API key is set, let claude CLI use OAuth/Pro)
25
+ if cfg.anthropic_api_key and not cfg.claude_pro_for_tasks:
26
+ env["ANTHROPIC_API_KEY"] = cfg.anthropic_api_key`;
27
+ if (!c.includes(OLD_ENV)) { console.error('fix_engine env block not found'); process.exit(1); }
28
+ c = c.replace(OLD_ENV, NEW_ENV);
29
+
30
+ // 3. After getting output, check for rate limits and alert Slack
31
+ const OLD_OUTPUT = ` output = (result.stdout or "") + (result.stderr or "")
32
+
33
+ if output.strip().upper().startswith("SKIP:"):`;
34
+ const NEW_OUTPUT = ` output = (result.stdout or "") + (result.stderr or "")
35
+
36
+ # Alert Slack immediately on rate-limit / auth failure — never stay silent
37
+ alert_if_rate_limited(
38
+ cfg.slack_bot_token,
39
+ cfg.slack_channel,
40
+ source=f"fix_engine/{event.fingerprint}",
41
+ output=output,
42
+ )
43
+
44
+ if output.strip().upper().startswith("SKIP:"):`;
45
+ if (!c.includes(OLD_OUTPUT)) { console.error('fix_engine output block not found'); process.exit(1); }
46
+ c = c.replace(OLD_OUTPUT, NEW_OUTPUT);
47
+
48
+ // 4. Also alert on FileNotFoundError (claude CLI missing)
49
+ const OLD_NOTFOUND = ` except FileNotFoundError:
50
+ logger.error("Claude Code binary not found at '%s'", cfg.claude_code_bin)
51
+ return "error", None, ""`;
52
+ const NEW_NOTFOUND = ' except FileNotFoundError:\n'
53
+ + ' msg = (\n'
54
+ + ' f":warning: *Sentinel \u2014 Claude CLI not found*\\n"\n'
55
+ + ' f"`{cfg.claude_code_bin}` not found. Run: `npm install -g @anthropic-ai/claude-code`\\n"\n'
56
+ + ' f"Fix engine is disabled until this is resolved."\n'
57
+ + ' )\n'
58
+ + ' logger.error("Claude Code binary not found at \'%s\'", cfg.claude_code_bin)\n'
59
+ + ' slack_alert(cfg.slack_bot_token, cfg.slack_channel, msg)\n'
60
+ + ' return "error", None, ""';
61
+ if (!c.includes(OLD_NOTFOUND)) { console.error('fix_engine notfound block not found'); process.exit(1); }
62
+ c = c.replace(OLD_NOTFOUND, NEW_NOTFOUND);
63
+
64
+ fs.writeFileSync(f, c, 'utf8');
65
+ console.log('fix_engine.py done. Lines:', c.split('\n').length);
66
+ }
67
+
68
+ // ── sentinel_boss.py ──────────────────────────────────────────────────────────
69
+ {
70
+ const f = 'sentinel/sentinel_boss.py';
71
+ let c = fs.readFileSync(f, 'utf8').replace(/\r\n/g, '\n');
72
+
73
+ // 1. Add notify import
74
+ const OLD_IMPORT = 'logger = logging.getLogger(__name__)';
75
+ const NEW_IMPORT = 'from .notify import alert_if_rate_limited, slack_alert, is_rate_limited\n\nlogger = logging.getLogger(__name__)';
76
+ if (!c.includes(OLD_IMPORT)) { console.error('boss import anchor not found'); process.exit(1); }
77
+ c = c.replace(OLD_IMPORT, NEW_IMPORT);
78
+
79
+ // 2. Revert auth priority: API key first, CLI fallback
80
+ // Replace current handle_message body
81
+ const OLD_HM_BODY =
82
+ ` # 1st priority: Claude Pro / OAuth via CLI
83
+ cli_reply, cli_done = await _handle_with_cli(
84
+ message, history, cfg_loader, store, slack_client=slack_client, user_name=user_name,
85
+ user_id=user_id, attachments=attachments,
86
+ )
87
+ if not cli_reply.startswith(":warning:"):
88
+ return cli_reply, cli_done
89
+
90
+ # CLI failed — try ANTHROPIC_API_KEY fallback
91
+ try:
92
+ import anthropic # noqa: F401
93
+ except ImportError:
94
+ return (
95
+ ":warning: \`anthropic\` package not installed. Run: \`pip install anthropic\`",
96
+ True,
97
+ )
98
+
99
+ api_key = cfg_loader.sentinel.anthropic_api_key or os.environ.get("ANTHROPIC_API_KEY", "")
100
+ if not api_key:
101
+ return cli_reply, cli_done # No fallback available
102
+
103
+ logger.info("Boss: CLI path failed (%s…), falling back to ANTHROPIC_API_KEY", cli_reply[:60])
104
+ return await _handle_with_api(
105
+ message, history, cfg_loader, store, slack_client=slack_client, user_name=user_name,
106
+ user_id=user_id, attachments=attachments,
107
+ )`;
108
+
109
+ const NEW_HM_BODY =
110
+ ` api_key = cfg_loader.sentinel.anthropic_api_key or os.environ.get("ANTHROPIC_API_KEY", "")
111
+
112
+ # 1st priority: ANTHROPIC_API_KEY — full structured tools, cheap per-token for Boss queries
113
+ if api_key:
114
+ try:
115
+ import anthropic # noqa: F401
116
+ return await _handle_with_api(
117
+ message, history, cfg_loader, store, slack_client=slack_client,
118
+ user_name=user_name, user_id=user_id, attachments=attachments,
119
+ )
120
+ except Exception as api_err:
121
+ err_str = str(api_err)
122
+ # Detect rate-limit / auth failure and alert Slack before falling through
123
+ cfg = cfg_loader.sentinel
124
+ if is_rate_limited(err_str):
125
+ from .notify import rate_limit_message
126
+ alert_if_rate_limited(cfg.slack_bot_token, cfg.slack_channel,
127
+ "sentinel_boss/api", err_str)
128
+ logger.warning("Boss: API key path failed (%s), trying CLI fallback", err_str[:80])
129
+
130
+ # 2nd priority: Claude Pro / OAuth via CLI (limited tools but no API key needed)
131
+ cli_reply, cli_done = await _handle_with_cli(
132
+ message, history, cfg_loader, store, slack_client=slack_client, user_name=user_name,
133
+ user_id=user_id, attachments=attachments,
134
+ )
135
+ if not cli_reply.startswith(":warning:"):
136
+ return cli_reply, cli_done
137
+
138
+ # Both paths failed — alert Slack and return error
139
+ cfg = cfg_loader.sentinel
140
+ err_output = cli_reply
141
+ alert_if_rate_limited(cfg.slack_bot_token, cfg.slack_channel,
142
+ "sentinel_boss/cli", err_output)
143
+ if not api_key:
144
+ # No auth at all configured
145
+ no_auth_msg = (
146
+ ":warning: *Sentinel Boss — no Claude auth configured*\\n"
147
+ "Configure at least one of:\\n"
148
+ "\u2022 \`ANTHROPIC_API_KEY\` in \`sentinel.properties\` \u2014 full features\\n"
149
+ "\u2022 Claude Pro OAuth: run \`claude login\` on the server \u2014 required for fix_engine\\n"
150
+ "See: https://github.com/misterhuydo/Sentinel#authentication"
151
+ )
152
+ slack_alert(cfg.slack_bot_token, cfg.slack_channel, no_auth_msg)
153
+ return ":warning: No Claude authentication configured. See Slack for details.", True
154
+ return cli_reply, cli_done`;
155
+
156
+ if (!c.includes(OLD_HM_BODY)) { console.error('handle_message body not found'); process.exit(1); }
157
+ c = c.replace(OLD_HM_BODY, NEW_HM_BODY);
158
+
159
+ // 3. In _handle_with_cli, alert Slack on rate-limit detected in CLI output
160
+ const OLD_CLI_FAIL =
161
+ ` if result.returncode != 0 and not output:
162
+ return f":warning: \`claude --print\` failed (exit {result.returncode}): {(result.stderr or '').strip()[:300]}", True`;
163
+ const NEW_CLI_FAIL =
164
+ ` raw_err = (result.stderr or "").strip()
165
+ if result.returncode != 0 and not output:
166
+ full_err = f"exit {result.returncode}: {raw_err[:300]}"
167
+ cfg = cfg_loader.sentinel
168
+ alert_if_rate_limited(cfg.slack_bot_token, cfg.slack_channel,
169
+ "sentinel_boss/cli", raw_err or full_err)
170
+ return f":warning: \`claude --print\` failed ({full_err})", True`;
171
+ if (!c.includes(OLD_CLI_FAIL)) { console.error('cli fail block not found'); process.exit(1); }
172
+ c = c.replace(OLD_CLI_FAIL, NEW_CLI_FAIL);
173
+
174
+ // 4. Also alert in ask_codebase when claude --print fails
175
+ const OLD_ASK_FAIL = ` if r.returncode != 0 and not output:
176
+ return {"repo": repo_name, "error": f"claude --print failed (rc={r.returncode}): {(r.stderr or '')[:200]}"}`;
177
+ const NEW_ASK_FAIL = ` if r.returncode != 0 and not output:
178
+ raw_err = (r.stderr or "")
179
+ alert_if_rate_limited(
180
+ cfg.slack_bot_token, cfg.slack_channel,
181
+ f"ask_codebase/{repo_name}", raw_err,
182
+ )
183
+ return {"repo": repo_name, "error": f"claude --print failed (rc={r.returncode}): {raw_err[:200]}"}`;
184
+ if (!c.includes(OLD_ASK_FAIL)) { console.error('ask_codebase fail block not found'); process.exit(1); }
185
+ c = c.replace(OLD_ASK_FAIL, NEW_ASK_FAIL);
186
+
187
+ // 5. ask_codebase: respect claude_pro_for_tasks (don't inject API key when Pro preferred)
188
+ const OLD_ASK_ENV = ` env = os.environ.copy()
189
+ if cfg.anthropic_api_key:
190
+ env["ANTHROPIC_API_KEY"] = cfg.anthropic_api_key`;
191
+ const NEW_ASK_ENV = ` env = os.environ.copy()
192
+ # Only inject API key when Claude Pro is NOT preferred for heavy tasks
193
+ if cfg.anthropic_api_key and not cfg.claude_pro_for_tasks:
194
+ env["ANTHROPIC_API_KEY"] = cfg.anthropic_api_key`;
195
+ if (!c.includes(OLD_ASK_ENV)) { console.error('ask_codebase env not found'); process.exit(1); }
196
+ c = c.replace(OLD_ASK_ENV, NEW_ASK_ENV);
197
+
198
+ fs.writeFileSync(f, c, 'utf8');
199
+ console.log('sentinel_boss.py done. Lines:', c.split('\n').length);
200
+ }
@@ -63,6 +63,11 @@ class SentinelConfig:
63
63
  slack_watch_bot_ids: list[str] = field(default_factory=list) # pre-configured bot IDs to watch passively
64
64
  slack_allowed_users: list[str] = field(default_factory=list) # if set, only these Slack user IDs can talk to Boss
65
65
  project_name: str = "" # optional: friendly name used by Sentinel Boss (e.g. "1881")
66
+ # Auth strategy:
67
+ # ANTHROPIC_API_KEY — used by Sentinel Boss conversation (structured tools, cheap per-token)
68
+ # Claude Pro / OAuth — used by fix_engine + ask_codebase when CLAUDE_PRO_FOR_TASKS=true
69
+ # At least one must be configured. Both = ideal split (Boss=API key, heavy tasks=Pro).
70
+ claude_pro_for_tasks: bool = True # when True + API key set, fix_engine/ask_codebase use claude CLI (Pro billing)
66
71
 
67
72
 
68
73
  @dataclass
@@ -158,6 +163,7 @@ class ConfigLoader:
158
163
  c.slack_watch_bot_ids = _csv(d.get("SLACK_WATCH_BOT_IDS", ""))
159
164
  c.slack_allowed_users = _csv(d.get("SLACK_ALLOWED_USERS", ""))
160
165
  c.project_name = d.get("PROJECT_NAME", "")
166
+ c.claude_pro_for_tasks = d.get("CLAUDE_PRO_FOR_TASKS", "true").lower() != "false"
161
167
  self.sentinel = c
162
168
 
163
169
  def _load_log_sources(self):