@misterhuydo/sentinel 1.0.97 → 1.0.99

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-03-23T07:34:25.856Z",
3
- "checkpoint_at": "2026-03-23T07:34:25.857Z",
2
+ "message": "Auto-checkpoint at 2026-03-23T07:47:01.705Z",
3
+ "checkpoint_at": "2026-03-23T07:47:01.706Z",
4
4
  "active_files": [],
5
5
  "notes": [],
6
6
  "mtime_snapshot": {}
package/bin/sentinel.js CHANGED
@@ -1,86 +1,87 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const chalk = require('chalk');
5
- const [,, command = 'help', ...args] = process.argv;
6
-
7
- // Handle flags before the main switch
8
- if (command === '--version' || command === '-v') {
9
- const { version } = require('../package.json');
10
- console.log(version);
11
- process.exit(0);
12
- }
13
- if (command === '--help' || command === '-h') {
14
- printUsage();
15
- process.exit(0);
16
- }
17
-
18
- const BANNER = `
19
- ${chalk.cyan('███████╗███████╗███╗ ██╗████████╗██╗███╗ ██╗███████╗██╗')}
20
- ${chalk.cyan('██╔════╝██╔════╝████╗ ██║╚══██╔══╝██║████╗ ██║██╔════╝██║')}
21
- ${chalk.cyan('███████╗█████╗ ██╔██╗ ██║ ██║ ██║██╔██╗ ██║█████╗ ██║')}
22
- ${chalk.cyan('╚════██║██╔══╝ ██║╚██╗██║ ██║ ██║██║╚██╗██║██╔══╝ ██║')}
23
- ${chalk.cyan('███████║███████╗██║ ╚████║ ██║ ██║██║ ╚████║███████╗███████╗')}
24
- ${chalk.cyan('╚══════╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝')}
25
- ${chalk.gray(' Autonomous DevOps Agent')}
26
- `;
27
-
28
- async function main() {
29
- console.log(BANNER);
30
-
31
- switch (command) {
32
- case 'init':
33
- await require('../lib/init')();
34
- break;
35
- case 'add':
36
- await require('../lib/add')(args[0]);
37
- break;
38
- case 'upgrade': {
39
- let upgradeCmd;
40
- try {
41
- upgradeCmd = require('../lib/upgrade');
42
- } catch (loadErr) {
43
- // upgrade.js itself is broken — self-heal with a bare npm install
44
- console.log(chalk.yellow(' ⚠'), 'upgrade module failed to load (' + loadErr.message + ')');
45
- console.log(chalk.cyan(' →'), 'Running bare npm install to self-heal...');
46
- const { spawnSync } = require('child_process');
47
- const r = spawnSync('npm', ['install', '-g', '@misterhuydo/sentinel@latest'],
48
- { stdio: 'inherit' });
49
- if (r.status === 0) {
50
- console.log(chalk.green(' ✔'), 'Self-healed — run `sentinel upgrade` again to finish');
51
- } else {
52
- console.error(chalk.red(' ✖'), 'npm install failed — try: npm install -g @misterhuydo/sentinel');
53
- }
54
- process.exit(r.status || 0);
55
- }
56
- await upgradeCmd();
57
- break;
58
- }
59
- case 'help':
60
- default:
61
- printUsage();
62
- }
63
- }
64
-
65
- function printUsage() {
66
- const { version } = require('../package.json');
67
- console.log(`${chalk.bold('sentinel')} v${version} — Autonomous DevOps Agent
68
-
69
- ${chalk.bold('Usage:')}
70
- sentinel init Interactive setup — install everything and create workspace
71
- sentinel add <name> Add a blank project (fill config manually)
72
- sentinel add <git-url> Add a project pre-configured for a GitHub repo
73
- sentinel add <project.json> Add a project from a local JSON config file
74
- sentinel add <https://host/cfg.json> Add a project from a remote JSON config URL
75
- sentinel upgrade Pull latest version and hot-deploy Python source
76
-
77
- ${chalk.bold('Options:')}
78
- --version, -v Print version
79
- --help, -h Print this help
80
- `);
81
- }
82
-
83
- main().catch(err => {
84
- console.error(chalk.red('Error:'), err.message);
85
- process.exit(1);
86
- });
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const chalk = require('chalk');
5
+ const [,, command = 'help', ...args] = process.argv;
6
+
7
+ if (command === '--version' || command === '-v') {
8
+ const { version } = require('../package.json');
9
+ console.log(version);
10
+ process.exit(0);
11
+ }
12
+ if (command === '--help' || command === '-h') {
13
+ printUsage();
14
+ process.exit(0);
15
+ }
16
+
17
+ const BANNER = `
18
+ ${chalk.cyan('███████╗███████╗███╗ ██╗████████╗██╗███╗ ██╗███████╗██╗')}
19
+ ${chalk.cyan('██╔════╝██╔════╝████╗ ██║╚══██╔══╝██║████╗ ██║██╔════╝██║')}
20
+ ${chalk.cyan('███████╗█████╗ ██╔██╗ ██║ ██║ ██║██╔██╗ ██║█████╗ ██║')}
21
+ ${chalk.cyan('╚════██║██╔══╝ ██║╚██╗██║ ██║ ██║██║╚██╗██║██╔══╝ ██║')}
22
+ ${chalk.cyan('███████║███████╗██║ ╚████║ ██║ ██║██║ ╚████║███████╗███████╗')}
23
+ ${chalk.cyan('╚══════╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝')}
24
+ ${chalk.gray(' Autonomous DevOps Agent')}
25
+ `;
26
+
27
+ async function main() {
28
+ console.log(BANNER);
29
+ switch (command) {
30
+ case 'init':
31
+ await require('../lib/init')();
32
+ break;
33
+ case 'add':
34
+ await require('../lib/add')(args[0]);
35
+ break;
36
+ case 'test':
37
+ await require('../lib/test')(args[0]);
38
+ break;
39
+ case 'upgrade': {
40
+ let upgradeCmd;
41
+ try {
42
+ upgradeCmd = require('../lib/upgrade');
43
+ } catch (loadErr) {
44
+ console.log(chalk.yellow(' ⚠'), 'upgrade module failed to load (' + loadErr.message + ')');
45
+ console.log(chalk.cyan(' →'), 'Running bare npm install to self-heal...');
46
+ const { spawnSync } = require('child_process');
47
+ const r = spawnSync('npm', ['install', '-g', '@misterhuydo/sentinel@latest'],
48
+ { stdio: 'inherit' });
49
+ if (r.status === 0) {
50
+ console.log(chalk.green(' ✔'), 'Self-healed — run `sentinel upgrade` again to finish');
51
+ } else {
52
+ console.error(chalk.red(' ✖'), 'npm install failed — try: npm install -g @misterhuydo/sentinel');
53
+ }
54
+ process.exit(r.status || 0);
55
+ }
56
+ await upgradeCmd();
57
+ break;
58
+ }
59
+ case 'help':
60
+ default:
61
+ printUsage();
62
+ }
63
+ }
64
+
65
+ function printUsage() {
66
+ const { version } = require('../package.json');
67
+ console.log(`${chalk.bold('sentinel')} v${version} — Autonomous DevOps Agent
68
+
69
+ ${chalk.bold('Usage:')}
70
+ sentinel init Interactive setup — install everything and create workspace
71
+ sentinel add <name> Add a blank project (fill config manually)
72
+ sentinel add <git-url> Add a project pre-configured for a GitHub repo
73
+ sentinel add <project.json> Add a project from a local JSON config file
74
+ sentinel add <https://host/cfg.json> Add a project from a remote JSON config URL
75
+ sentinel test [project] Validate installation and config before going live
76
+ sentinel upgrade Pull latest version and hot-deploy Python source
77
+
78
+ ${chalk.bold('Options:')}
79
+ --version, -v Print version
80
+ --help, -h Print this help
81
+ `);
82
+ }
83
+
84
+ main().catch(err => {
85
+ console.error(chalk.red('Error:'), err.message);
86
+ process.exit(1);
87
+ });
package/lib/test.js ADDED
@@ -0,0 +1,208 @@
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
+
8
+ const ok = msg => console.log(chalk.green(' ✔'), msg);
9
+ const fail = msg => console.log(chalk.red(' ✖'), msg);
10
+ const warn = msg => console.log(chalk.yellow(' ⚠'), msg);
11
+ const info = msg => console.log(chalk.cyan(' →'), msg);
12
+
13
+ module.exports = async function testInstall(projectName) {
14
+ const defaultWorkspace = path.join(os.homedir(), 'sentinel');
15
+ const projectDir = projectName
16
+ ? path.join(defaultWorkspace, projectName)
17
+ : _findActiveProject(defaultWorkspace);
18
+
19
+ console.log(chalk.bold('\nSentinel — Installation Check\n'));
20
+
21
+ let passed = 0;
22
+ let failed = 0;
23
+
24
+ // ── 1. Tools ──────────────────────────────────────────────────────────────
25
+ info('Checking required tools...');
26
+
27
+ const tools = [
28
+ { cmd: 'python3 --version', label: 'Python 3' },
29
+ { cmd: 'node --version', label: 'Node.js' },
30
+ { cmd: 'git --version', label: 'git' },
31
+ ];
32
+ for (const { cmd, label } of tools) {
33
+ try {
34
+ const out = execSync(cmd, { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
35
+ ok(`${label}: ${out}`);
36
+ passed++;
37
+ } catch {
38
+ fail(`${label} not found`);
39
+ failed++;
40
+ }
41
+ }
42
+
43
+ // ── 2. npm globals ────────────────────────────────────────────────────────
44
+ info('Checking npm globals...');
45
+
46
+ const npms = [
47
+ { cmd: 'sentinel --version', label: '@misterhuydo/sentinel' },
48
+ { cmd: 'cairn --version', label: '@misterhuydo/cairn-mcp' },
49
+ { cmd: 'claude --version', label: '@anthropic-ai/claude-code'},
50
+ ];
51
+ for (const { cmd, label } of npms) {
52
+ try {
53
+ const out = execSync(cmd, { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
54
+ ok(`${label}: ${out}`);
55
+ passed++;
56
+ } catch {
57
+ fail(`${label} not installed — run: npm install -g ${label}`);
58
+ failed++;
59
+ }
60
+ }
61
+
62
+ // ── 3. Claude auth ────────────────────────────────────────────────────────
63
+ info('Checking Claude auth...');
64
+ const apiKey = _readSentinelProp(projectDir, 'ANTHROPIC_API_KEY')
65
+ || process.env.ANTHROPIC_API_KEY || '';
66
+ const claudeProTasks = (_readSentinelProp(projectDir, 'CLAUDE_PRO_FOR_TASKS') || 'true').toLowerCase() !== 'false';
67
+
68
+ if (apiKey) {
69
+ ok(`ANTHROPIC_API_KEY configured (${apiKey.slice(0, 12)}...)`);
70
+ passed++;
71
+ } else {
72
+ warn('ANTHROPIC_API_KEY not set — Sentinel Boss will use CLI fallback only');
73
+ }
74
+
75
+ try {
76
+ const r = spawnSync('claude', ['--version'], { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] });
77
+ if (r.status === 0) {
78
+ ok(`claude CLI available: ${(r.stdout || '').trim()}`);
79
+ if (claudeProTasks) {
80
+ ok('CLAUDE_PRO_FOR_TASKS=true — fix_engine will use Claude Pro subscription');
81
+ }
82
+ passed++;
83
+ } else {
84
+ warn('claude CLI found but --version failed');
85
+ }
86
+ } catch {
87
+ fail('claude CLI not found');
88
+ failed++;
89
+ }
90
+
91
+ if (!apiKey) {
92
+ // Try a quick login check
93
+ try {
94
+ const r = spawnSync('claude', ['--print', 'ping', '--no-interactive'],
95
+ { encoding: 'utf8', timeout: 10000, stdio: ['pipe','pipe','pipe'] });
96
+ if (r.status === 0) {
97
+ ok('claude OAuth session active');
98
+ passed++;
99
+ } else {
100
+ warn('claude OAuth session may be expired — run: claude login');
101
+ }
102
+ } catch {
103
+ warn('Could not verify claude OAuth session');
104
+ }
105
+ }
106
+
107
+ // ── 4. Project config ─────────────────────────────────────────────────────
108
+ if (projectDir && fs.existsSync(projectDir)) {
109
+ info(`Checking project config at ${projectDir}...`);
110
+
111
+ const sentinelProps = path.join(projectDir, 'config', 'sentinel.properties');
112
+ if (fs.existsSync(sentinelProps)) {
113
+ ok('sentinel.properties found');
114
+ passed++;
115
+ } else {
116
+ fail(`sentinel.properties missing at ${sentinelProps}`);
117
+ failed++;
118
+ }
119
+
120
+ const logConfigs = path.join(projectDir, 'config', 'log-configs');
121
+ const repoConfigs = path.join(projectDir, 'config', 'repo-configs');
122
+
123
+ const logCount = fs.existsSync(logConfigs) ? fs.readdirSync(logConfigs).filter(f => f.endsWith('.properties') && !f.startsWith('_')).length : 0;
124
+ const repoCount = fs.existsSync(repoConfigs) ? fs.readdirSync(repoConfigs).filter(f => f.endsWith('.properties') && !f.startsWith('_')).length : 0;
125
+
126
+ if (logCount > 0) { ok(`${logCount} log-config(s) found`); passed++; }
127
+ else { warn('No log-configs found — add at least one in config/log-configs/'); }
128
+
129
+ if (repoCount > 0) { ok(`${repoCount} repo-config(s) found`); passed++; }
130
+ else { warn('No repo-configs found — add at least one in config/repo-configs/'); }
131
+
132
+ // GitHub token
133
+ const ghToken = _readSentinelProp(projectDir, 'GITHUB_TOKEN') || '';
134
+ if (ghToken) { ok('GITHUB_TOKEN configured'); passed++; }
135
+ else { warn('GITHUB_TOKEN not set — cannot open PRs'); }
136
+
137
+ // Slack
138
+ const slackBot = _readSentinelProp(projectDir, 'SLACK_BOT_TOKEN') || '';
139
+ const slackApp = _readSentinelProp(projectDir, 'SLACK_APP_TOKEN') || '';
140
+ if (slackBot && slackApp) { ok('Slack tokens configured (Boss enabled)'); passed++; }
141
+ else { warn('Slack tokens not set — Boss disabled'); }
142
+
143
+ } else if (projectName) {
144
+ fail(`Project '${projectName}' not found at ${projectDir}`);
145
+ failed++;
146
+ } else {
147
+ warn('No project specified — skipping project config checks');
148
+ warn('Run: sentinel test <project-name>');
149
+ }
150
+
151
+ // ── 5. Python deps ────────────────────────────────────────────────────────
152
+ info('Checking Python dependencies...');
153
+ const codeDir = path.join(defaultWorkspace, 'code');
154
+ const reqFile = path.join(codeDir, 'requirements.txt');
155
+ if (fs.existsSync(reqFile)) {
156
+ try {
157
+ execSync('python3 -c "import paramiko, schedule, requests, jinja2"',
158
+ { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] });
159
+ ok('Python dependencies installed');
160
+ passed++;
161
+ } catch {
162
+ fail('Python dependencies missing — run: pip install -r requirements.txt');
163
+ failed++;
164
+ }
165
+ } else {
166
+ warn('requirements.txt not found — run: sentinel init');
167
+ }
168
+
169
+ // ── Summary ───────────────────────────────────────────────────────────────
170
+ console.log('');
171
+ if (failed === 0) {
172
+ console.log(chalk.green.bold(` ✔ All checks passed (${passed} ok)`));
173
+ } else {
174
+ console.log(chalk.yellow.bold(` ${passed} passed, ${failed} failed`));
175
+ console.log(chalk.gray(' Fix the issues above before starting Sentinel.'));
176
+ }
177
+ console.log('');
178
+ process.exit(failed > 0 ? 1 : 0);
179
+ };
180
+
181
+
182
+ function _findActiveProject(workspace) {
183
+ if (!fs.existsSync(workspace)) return null;
184
+ const dirs = fs.readdirSync(workspace)
185
+ .map(d => path.join(workspace, d))
186
+ .filter(d => fs.statSync(d).isDirectory()
187
+ && fs.existsSync(path.join(d, 'config', 'sentinel.properties')));
188
+ return dirs.length === 1 ? dirs[0] : null;
189
+ }
190
+
191
+ function _readSentinelProp(projectDir, key) {
192
+ if (!projectDir) return '';
193
+ // Try project-level first, then workspace-level
194
+ const candidates = [
195
+ path.join(projectDir, 'config', 'sentinel.properties'),
196
+ path.join(os.homedir(), 'sentinel', 'sentinel.properties'),
197
+ ];
198
+ for (const f of candidates) {
199
+ if (!fs.existsSync(f)) continue;
200
+ for (const line of fs.readFileSync(f, 'utf8').split('\n')) {
201
+ const stripped = line.trim();
202
+ if (stripped.startsWith('#') || !stripped.includes('=')) continue;
203
+ const [k, ...rest] = stripped.split('=');
204
+ if (k.trim() === key) return rest.join('=').split('#')[0].trim();
205
+ }
206
+ }
207
+ return '';
208
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.0.97",
3
+ "version": "1.0.99",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -179,6 +179,22 @@ When to act vs. when to ask:
179
179
  - Unclear intent (could be either) → use judgment: brief explanation + "Want me to run this now?"
180
180
  Never say "Stand by" or "Requesting..." and then return nothing. Either act, or ask.
181
181
 
182
+ Issue identification — before calling create_issue:
183
+ 1. Determine if the message is a REAL issue/task (bug report, feature request, investigation ask)
184
+ vs. a status question, tool query, or casual chat. If not an issue, just answer normally.
185
+ 2. If it IS an issue, gather what's needed before creating:
186
+ - Project: which project? If unclear, ask. Use list_projects if you need to check names.
187
+ - Context: what's the problem? Include everything: description, error text, steps to reproduce.
188
+ - Attachments: summarise any files/screenshots the user shared.
189
+ - Support URL: note any ticket/doc/link the user mentioned.
190
+ - Identity: always captured automatically from the Slack session.
191
+ 3. Before calling the tool, confirm with the user in natural language:
192
+ e.g. "I'll create an issue for project *1881* — here's what I have: [summary]. Look right?"
193
+ Wait for their confirmation before proceeding.
194
+ EXCEPTION: if the user's message already contains a clear project + unambiguous description,
195
+ skip the confirmation and create immediately — don't ask when nothing is unclear.
196
+ 4. After creating, tell them the issue was queued and Sentinel will pick it up on the next poll.
197
+
182
198
  When the engineer's request is fully handled, end your LAST message with the token: [DONE]
183
199
  IMPORTANT: Always write your actual reply text FIRST, then append [DONE] at the end. Example: "Hello! I'm Sentinel. [DONE]". Never output [DONE] as your only content.
184
200
  For greetings like "hello" or empty messages, introduce yourself briefly and offer help, then end with [DONE].
@@ -209,26 +225,35 @@ _TOOLS = [
209
225
  {
210
226
  "name": "create_issue",
211
227
  "description": (
212
- "Deliver a fix/task request to a Sentinel project instance. "
213
- "Use when the engineer says 'tell 1881 to do X', 'look into Y in project elprint', "
214
- "'implement this in 1881: ...'. Can target any project by short name. "
215
- "Defaults to the current project if no project is specified."
228
+ "Deliver a confirmed issue/task to a Sentinel project instance. "
229
+ "Only call this after you have: (1) confirmed the message is a real issue or task, "
230
+ "(2) identified the target project, (3) gathered enough context, and "
231
+ "(4) confirmed with the user ('I'll create this issue for project X — does that look right?'). "
232
+ "Do NOT call this for status questions, tool queries, or casual chat."
216
233
  ),
217
234
  "input_schema": {
218
235
  "type": "object",
219
236
  "properties": {
220
237
  "description": {
221
238
  "type": "string",
222
- "description": "Full task/problem description — everything the engineer told you",
239
+ "description": "Full problem/task description — include all context the user gave you",
223
240
  },
224
241
  "project": {
225
242
  "type": "string",
226
- "description": "Project short name to deliver to (e.g. '1881', 'elprint'). Omit for current project.",
243
+ "description": "Project short name (e.g. '1881', 'elprint'). Ask if unclear.",
227
244
  },
228
245
  "target_repo": {
229
246
  "type": "string",
230
247
  "description": "Specific repo within the project (omit to let Sentinel auto-route)",
231
248
  },
249
+ "support_url": {
250
+ "type": "string",
251
+ "description": "Any URL the user shared (ticket, doc, screenshot link, etc.)",
252
+ },
253
+ "attachments_summary": {
254
+ "type": "string",
255
+ "description": "Summary of any files/screenshots the user attached",
256
+ },
232
257
  },
233
258
  "required": ["description"],
234
259
  },
@@ -796,10 +821,28 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
796
821
  else:
797
822
  project_dir = Path(".")
798
823
 
824
+ support_url = inputs.get("support_url", "").strip()
825
+ attachments_summary = inputs.get("attachments_summary", "").strip()
826
+
799
827
  issues_dir = project_dir / "issues"
800
828
  issues_dir.mkdir(exist_ok=True)
801
- fname = f"slack-{uuid.uuid4().hex[:8]}.txt"
802
- content = (f"TARGET_REPO: {target_repo}\n\n" if target_repo else "") + description
829
+ fname = f"slack-{uuid.uuid4().hex[:8]}.txt"
830
+
831
+ submitter_name = store.get_user_name(user_id) if user_id else ""
832
+ submitter_line = f"SUBMITTED_BY: {submitter_name} ({user_id})" if user_id else ""
833
+ lines = []
834
+ if submitter_line:
835
+ lines.append(submitter_line)
836
+ if target_repo:
837
+ lines.append(f"TARGET_REPO: {target_repo}")
838
+ if support_url:
839
+ lines.append(f"SUPPORT_URL: {support_url}")
840
+ lines.append(f"SUBMITTED_AT: {datetime.now(timezone.utc).isoformat()}")
841
+ lines.append("")
842
+ lines.append(description)
843
+ if attachments_summary:
844
+ lines.append(f"\nATTACHMENTS:\n{attachments_summary}")
845
+ content = "\n".join(lines)
803
846
  (issues_dir / fname).write_text(content, encoding="utf-8")
804
847
 
805
848
  # Touch SENTINEL_POLL_NOW so the target instance picks it up immediately
@@ -811,7 +854,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
811
854
  try:
812
855
  store.record_submitted_issue(
813
856
  user_id=user_id,
814
- user_name="", # resolved by caller if needed
857
+ user_name=submitter_name,
815
858
  project=project_label,
816
859
  fname=fname,
817
860
  description=description,
@@ -1610,6 +1653,7 @@ async def _handle_with_cli(
1610
1653
  + f"\nManaged repos: {', '.join(repos) if repos else '(none configured)'}"
1611
1654
  + (f"\nLog sources: {', '.join(log_sources)}" if log_sources else "")
1612
1655
  + f"\nAdmin access for this user: {'YES — admin tools are available' if is_admin else 'NO — admin tools will be refused'}"
1656
+ + "\nNOTE: Running in CLI fallback mode — admin tools and some features are unavailable. Ask user to configure ANTHROPIC_API_KEY for full features."
1613
1657
  + f"\n\nCurrent status (last 24 h):\n{status_json}"
1614
1658
  + f"\n\nOpen PRs:\n{prs_json}"
1615
1659
  + (f"\n\nLog search results:\n{search_json}" if search_json else "")
@@ -1751,6 +1795,9 @@ async def _handle_with_api(
1751
1795
  if not reply:
1752
1796
  greeting = f"Hi {user_name}! " if user_name else "Hi! "
1753
1797
  reply = f"{greeting}I'm Sentinel, your autonomous DevOps agent. How can I help you?"
1798
+ # Heuristic override: if reply ends with a question, Claude is waiting for input
1799
+ if is_done and re.search(r'\?\s*$', reply):
1800
+ is_done = False
1754
1801
  history.append({"role": "assistant", "content": response.content})
1755
1802
  return reply, is_done
1756
1803
 
@@ -1,58 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- const chalk = require('chalk');
4
- const [,, command = 'help', ...args] = process.argv;
5
- if (command === '--version' || command === '-v') {
6
- const { version } = require('../package.json');
7
- console.log(version);
8
- process.exit(0);
9
- }
10
- if (command === '--help' || command === '-h') {
11
- printUsage();
12
- process.exit(0);
13
- }
14
- const BANNER = `
15
- ${chalk.cyan('███████╗███████╗███╗ ██╗████████╗██╗███╗ ██╗███████╗██╗')}
16
- ${chalk.cyan('██╔════╝██╔════╝████╗ ██║╚══██╔══╝██║████╗ ██║██╔════╝██║')}
17
- ${chalk.cyan('███████╗█████╗ ██╔██╗ ██║ ██║ ██║██╔██╗ ██║█████╗ ██║')}
18
- ${chalk.cyan('╚════██║██╔══╝ ██║╚██╗██║ ██║ ██║██║╚██╗██║██╔══╝ ██║')}
19
- ${chalk.cyan('███████║███████╗██║ ╚████║ ██║ ██║██║ ╚████║███████╗███████╗')}
20
- ${chalk.cyan('╚══════╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝')}
21
- ${chalk.gray(' Autonomous DevOps Agent')}
22
- `;
23
- async function main() {
24
- console.log(BANNER);
25
- switch (command) {
26
- case 'init':
27
- await require('../lib/init')();
28
- break;
29
- case 'add':
30
- await require('../lib/add')(args[0]);
31
- break;
32
- case 'upgrade':
33
- await require('../lib/upgrade')();
34
- break;
35
- case 'help':
36
- default:
37
- printUsage();
38
- }
39
- }
40
- function printUsage() {
41
- const { version } = require('../package.json');
42
- console.log(`${chalk.bold('sentinel')} v${version} — Autonomous DevOps Agent
43
- ${chalk.bold('Usage:')}
44
- sentinel init Interactive setup — install everything and create workspace
45
- sentinel add <name> Add a blank project (fill config manually)
46
- sentinel add <git-url> Add a project pre-configured for a GitHub repo
47
- sentinel add <project.json> Add a project from a local JSON config file
48
- sentinel add <https://host/cfg.json> Add a project from a remote JSON config URL
49
- sentinel upgrade Pull latest version and hot-deploy Python source
50
- ${chalk.bold('Options:')}
51
- --version, -v Print version
52
- --help, -h Print this help
53
- `);
54
- }
55
- main().catch(err => {
56
- console.error(chalk.red('Error:'), err.message);
57
- process.exit(1);
58
- });