@misterhuydo/sentinel 1.0.82 → 1.0.84

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-22T17:57:09.109Z
1
+ 2026-03-23T05:01:48.297Z
@@ -4,11 +4,5 @@
4
4
  "state": "compressed",
5
5
  "minifiedAt": 1774128147034.2527,
6
6
  "readCount": 1
7
- },
8
- "J:\\Projects\\Sentinel\\cli\\lib\\init.js": {
9
- "tempPath": "J:\\Projects\\Sentinel\\cli\\.cairn\\views\\2a85cc_init.js",
10
- "state": "compressed",
11
- "minifiedAt": 1774189651415.41,
12
- "readCount": 1
13
7
  }
14
8
  }
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-03-22T17:52:08.090Z",
3
- "checkpoint_at": "2026-03-22T17:52:08.091Z",
2
+ "message": "Auto-checkpoint at 2026-03-23T05:23:48.069Z",
3
+ "checkpoint_at": "2026-03-23T05:23:48.071Z",
4
4
  "active_files": [],
5
5
  "notes": [],
6
6
  "mtime_snapshot": {}
package/lib/generate.js CHANGED
@@ -96,7 +96,7 @@ rm -f "$PID_FILE"
96
96
 
97
97
  // ── Workspace-level startAll / stopAll ────────────────────────────────────────
98
98
 
99
- function generateWorkspaceScripts(workspace, smtpConfig = {}, slackConfig = {}) {
99
+ function generateWorkspaceScripts(workspace, smtpConfig = {}, slackConfig = {}, authConfig = {}) {
100
100
  // Write shared sentinel.properties once (never overwrite existing)
101
101
  const workspaceProps = path.join(workspace, 'sentinel.properties');
102
102
  if (!fs.existsSync(workspaceProps)) {
@@ -108,6 +108,20 @@ function generateWorkspaceScripts(workspace, smtpConfig = {}, slackConfig = {})
108
108
  if (smtpConfig.password) tpl = tpl.replace('SMTP_PASSWORD=<app-password>', 'SMTP_PASSWORD=' + smtpConfig.password);
109
109
  fs.writeFileSync(workspaceProps, tpl);
110
110
  }
111
+ // Always upsert auth config so re-runs persist it
112
+ if (authConfig.apiKey || authConfig.claudeProForTasks !== undefined) {
113
+ let props = fs.readFileSync(workspaceProps, 'utf8');
114
+ if (authConfig.apiKey) {
115
+ const replaced = props.replace(/^#?\s*ANTHROPIC_API_KEY=.*/m, 'ANTHROPIC_API_KEY=' + authConfig.apiKey);
116
+ props = replaced !== props ? replaced : props.trimEnd() + '\nANTHROPIC_API_KEY=' + authConfig.apiKey + '\n';
117
+ }
118
+ if (authConfig.claudeProForTasks !== undefined) {
119
+ const val = authConfig.claudeProForTasks ? 'true' : 'false';
120
+ const replaced = props.replace(/^CLAUDE_PRO_FOR_TASKS=.*/m, 'CLAUDE_PRO_FOR_TASKS=' + val);
121
+ props = replaced !== props ? replaced : props.trimEnd() + '\nCLAUDE_PRO_FOR_TASKS=' + val + '\n';
122
+ }
123
+ fs.writeFileSync(workspaceProps, props);
124
+ }
111
125
  // Always upsert Slack tokens so re-runs persist them
112
126
  if (slackConfig.botToken || slackConfig.appToken) {
113
127
  let props = fs.readFileSync(workspaceProps, 'utf8');
package/lib/init.js CHANGED
@@ -33,15 +33,26 @@ module.exports = async function init() {
33
33
  {
34
34
  type: 'select',
35
35
  name: 'authMode',
36
- message: 'How will Claude Code authenticate?',
36
+ message: 'Claude authentication strategy',
37
+ hint: 'Boss = Slack AI; Fix Engine = autonomous code repair',
37
38
  choices: [
38
- { title: 'API key (Anthropic API account — recommended for servers)', value: 'apikey' },
39
- { title: 'Claude Pro / OAuth (will give you a URL to open in any browser)', value: 'oauth' },
40
- { title: 'Skip (I will configure this later)', value: 'skip' },
39
+ {
40
+ title: 'Both (RECOMMENDED) API key for Boss, Claude Pro for Fix Engine',
41
+ value: 'both',
42
+ },
43
+ {
44
+ title: 'API key only — full Boss tools; Fix Engine billed per token',
45
+ value: 'apikey',
46
+ },
47
+ {
48
+ title: 'Claude Pro / OAuth only — run `claude login`; Boss has limited tools',
49
+ value: 'oauth',
50
+ },
51
+ { title: 'Skip (configure manually in sentinel.properties)', value: 'skip' },
41
52
  ],
42
53
  },
43
54
  {
44
- type: prev => prev === 'apikey' ? 'password' : null,
55
+ type: prev => (prev === 'apikey' || prev === 'both') ? 'password' : null,
45
56
  name: 'anthropicKey',
46
57
  message: 'Anthropic API key (sk-ant-...)',
47
58
  validate: v => v.startsWith('sk-ant-') ? true : 'Key should start with sk-ant-',
@@ -147,12 +158,22 @@ module.exports = async function init() {
147
158
 
148
159
  // ── Claude Code auth ─────────────────────────────────────────────────────────
149
160
  step('Claude Code authentication…');
150
- if (authMode === 'apikey' && anthropicKey) {
151
- ok('API key will be written to each project\'s sentinel.properties');
161
+ if (authMode === 'both' && anthropicKey) {
162
+ ok('API key Sentinel Boss (full tools, structured responses)');
163
+ ok('Claude Pro → Fix Engine + Ask Codebase (heavy coding, Pro subscription)');
164
+ info('Run `claude login` on this server now (or before starting projects)');
165
+ info('CLAUDE_PRO_FOR_TASKS=true written to workspace sentinel.properties');
166
+ } else if (authMode === 'apikey' && anthropicKey) {
167
+ ok('API key → all Claude usage (Boss + Fix Engine billed to your API quota)');
168
+ info('CLAUDE_PRO_FOR_TASKS=false written — Fix Engine will use API key');
169
+ warn('Heavy fix tasks will consume API tokens. Claude Pro is cheaper for those.');
152
170
  } else if (authMode === 'oauth') {
153
- info('OAuth selected start.sh will prompt for login if not yet authenticated');
171
+ ok('Claude Pro / OAuth Fix Engine + Ask Codebase');
172
+ warn('Boss will use CLI fallback — some tools unavailable without an API key');
173
+ info('Run `claude login` on this server to authenticate');
154
174
  } else {
155
- info('Skipping auth — start.sh will prompt for login if needed');
175
+ warn('No auth configured add ANTHROPIC_API_KEY or run `claude login` before starting');
176
+ info('See: workspace sentinel.properties for full auth documentation');
156
177
  }
157
178
 
158
179
  // ── Slack Bot ────────────────────────────────────────────────────────────────
@@ -179,7 +200,11 @@ module.exports = async function init() {
179
200
 
180
201
  // ── Workspace start/stop scripts ─────────────────────────────────────────────
181
202
  step('Generating scripts…');
182
- generateWorkspaceScripts(workspace, { host: smtpHost, user: smtpUser, password: effectiveSmtpPassword }, { botToken: effectiveSlackBotToken, appToken: effectiveSlackAppToken });
203
+ const authConfig = {};
204
+ if (anthropicKey) authConfig.apiKey = anthropicKey;
205
+ if (authMode === 'both' || authMode === 'oauth') authConfig.claudeProForTasks = true;
206
+ if (authMode === 'apikey') authConfig.claudeProForTasks = false;
207
+ generateWorkspaceScripts(workspace, { host: smtpHost, user: smtpUser, password: effectiveSmtpPassword }, { botToken: effectiveSlackBotToken, appToken: effectiveSlackAppToken }, authConfig);
183
208
  ok(`${workspace}/startAll.sh`);
184
209
  ok(`${workspace}/stopAll.sh`);
185
210
 
@@ -316,8 +341,7 @@ function ensureClaudePermissions() {
316
341
  }
317
342
  try {
318
343
  fs.ensureDirSync(require('path').dirname(settingsPath));
319
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '
320
- ');
344
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
321
345
  ok('Claude Code permissions patched: ' + added.join(', '));
322
346
  } catch (e) {
323
347
  warn('Could not write ' + settingsPath + ': ' + e.message);
package/lib/upgrade.js CHANGED
@@ -28,8 +28,7 @@ function ensureClaudePermissions() {
28
28
  try {
29
29
  const path = require('path');
30
30
  fs.ensureDirSync(path.dirname(settingsPath));
31
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '
32
- ');
31
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
33
32
  ok('Claude Code permissions patched: ' + added.join(', '));
34
33
  } catch (e) {
35
34
  warn('Could not write ' + settingsPath + ': ' + e.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.0.82",
3
+ "version": "1.0.84",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -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):