@misterhuydo/sentinel 1.0.61 → 1.0.63

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,8 +1,14 @@
1
1
  {
2
2
  "J:\\Projects\\Sentinel\\cli\\lib\\upgrade.js": {
3
3
  "tempPath": "J:\\Projects\\Sentinel\\cli\\.cairn\\views\\fb78ac_upgrade.js",
4
- "state": "compressed",
4
+ "state": "edit-ready",
5
5
  "minifiedAt": 1774129312316.9353,
6
6
  "readCount": 1
7
+ },
8
+ "J:\\Projects\\Sentinel\\cli\\bin\\sentinel.js": {
9
+ "tempPath": "J:\\Projects\\Sentinel\\cli\\.cairn\\views\\a348d8_sentinel.js",
10
+ "state": "compressed",
11
+ "minifiedAt": 1774128147034.2527,
12
+ "readCount": 1
7
13
  }
8
14
  }
@@ -0,0 +1,212 @@
1
+ 'use strict';
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ function writeExampleProject(projectDir, codeDir, pythonBin, anthropicKey = '', slackTokens = {}) {
5
+ const configDir = path.join(projectDir, 'config', 'log-configs');
6
+ const repoDir = path.join(projectDir, 'config', 'repo-configs');
7
+ fs.ensureDirSync(configDir);
8
+ fs.ensureDirSync(repoDir);
9
+ const tplDir = path.join(__dirname, '..', 'templates');
10
+ let sentinelProps = fs.readFileSync(path.join(tplDir, 'sentinel.properties'), 'utf8');
11
+ if (anthropicKey) {
12
+ sentinelProps = sentinelProps.replace(/^# ANTHROPIC_API_KEY=.*/m, `ANTHROPIC_API_KEY=${anthropicKey}`);
13
+ }
14
+ if (slackTokens.botToken) {
15
+ sentinelProps = sentinelProps.replace(/^# SLACK_BOT_TOKEN=.*/m, `SLACK_BOT_TOKEN=${slackTokens.botToken}`);
16
+ }
17
+ if (slackTokens.appToken) {
18
+ sentinelProps = sentinelProps.replace(/^# SLACK_APP_TOKEN=.*/m, `SLACK_APP_TOKEN=${slackTokens.appToken}`);
19
+ }
20
+ fs.writeFileSync(path.join(projectDir, 'config', 'sentinel.properties'), sentinelProps);
21
+ fs.copySync(path.join(tplDir, 'log-configs', '_example.properties'), path.join(configDir, '_example.properties'));
22
+ fs.copySync(path.join(tplDir, 'repo-configs', '_example.properties'), path.join(repoDir, '_example.properties'));
23
+ generateProjectScripts(projectDir, codeDir, pythonBin);
24
+ }
25
+ function generateProjectScripts(projectDir, codeDir, pythonBin) {
26
+ const name = path.basename(projectDir);
27
+ fs.writeFileSync(path.join(projectDir, 'start.sh'), `#!/usr/bin/env bash
28
+ # Start this Sentinel instance
29
+ set -euo pipefail
30
+ DIR="$(cd "$(dirname "$0")" && pwd)"
31
+ PID_FILE="$DIR/sentinel.pid"
32
+ if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
33
+ echo "[sentinel] ${name} already running (PID $(cat "$PID_FILE"))"
34
+ exit 0
35
+ fi
36
+ # Kill any orphaned sentinel processes for this project (stale PIDs not in PID file)
37
+ pkill -f "sentinel.main --config ${DIR}/config" 2>/dev/null || true
38
+ rm -f "$PID_FILE"
39
+ # Check Claude Code authentication
40
+ AUTH_OUT=$(claude --print \"hi\" 2>&1 || true)
41
+ if echo "$AUTH_OUT" | grep -Eqi "not logged in|/login"; then
42
+ echo ""
43
+ echo "[sentinel] Claude Code is not authenticated."
44
+ echo " 1. Open a new terminal and run: claude"
45
+ echo " 2. Type /login at the prompt"
46
+ echo " 3. Open the URL in any browser and log in"
47
+ echo " 4. Type /exit when done"
48
+ echo " 5. Re-run this script"
49
+ echo ""
50
+ exit 1
51
+ fi
52
+ mkdir -p "$DIR/logs" "$DIR/workspace/fetched" "$DIR/workspace/patches" "$DIR/issues"
53
+ cd "$DIR"
54
+ PYTHONPATH="${codeDir}" "${pythonBin}" -m sentinel.main --config ./config \\
55
+ >> "$DIR/logs/sentinel.log" 2>&1 &
56
+ echo $! > "$PID_FILE"
57
+ echo "[sentinel] ${name} started (PID $!)"
58
+ `, { mode: 0o755 });
59
+ fs.writeFileSync(path.join(projectDir, 'stop.sh'), `#!/usr/bin/env bash
60
+ # Stop this Sentinel instance
61
+ set -euo pipefail
62
+ DIR="$(cd "$(dirname "$0")" && pwd)"
63
+ PID_FILE="$DIR/sentinel.pid"
64
+ if [[ ! -f "$PID_FILE" ]]; then
65
+ echo "[sentinel] ${name} — no PID file, not running"
66
+ exit 0
67
+ fi
68
+ PID=$(cat "$PID_FILE")
69
+ if kill -0 "$PID" 2>/dev/null; then
70
+ kill "$PID"
71
+ echo "[sentinel] ${name} stopped (PID $PID)"
72
+ else
73
+ echo "[sentinel] ${name} — PID $PID not running"
74
+ fi
75
+ rm -f "$PID_FILE"
76
+ `, { mode: 0o755 });
77
+ }
78
+ function generateWorkspaceScripts(workspace, smtpConfig = {}, slackConfig = {}) {
79
+ const workspaceProps = path.join(workspace, 'sentinel.properties');
80
+ if (!fs.existsSync(workspaceProps)) {
81
+ const tplDir = path.join(__dirname, '..', 'templates');
82
+ let tpl = fs.readFileSync(path.join(tplDir, 'workspace-sentinel.properties'), 'utf8');
83
+ if (smtpConfig.host) tpl = tpl.replace('SMTP_HOST=smtp.gmail.com', 'SMTP_HOST=' + smtpConfig.host);
84
+ if (smtpConfig.port) tpl = tpl.replace('SMTP_PORT=587', 'SMTP_PORT=' + smtpConfig.port);
85
+ if (smtpConfig.user) tpl = tpl.replace('SMTP_USER=sentinel@yourdomain.com', 'SMTP_USER=' + smtpConfig.user);
86
+ if (smtpConfig.password) tpl = tpl.replace('SMTP_PASSWORD=<app-password>', 'SMTP_PASSWORD=' + smtpConfig.password);
87
+ fs.writeFileSync(workspaceProps, tpl);
88
+ }
89
+ if (slackConfig.botToken || slackConfig.appToken) {
90
+ let props = fs.readFileSync(workspaceProps, 'utf8');
91
+ if (slackConfig.botToken) {
92
+ const replaced = props.replace(/^#?\s*SLACK_BOT_TOKEN=.*/m, 'SLACK_BOT_TOKEN=' + slackConfig.botToken);
93
+ props = replaced !== props ? replaced : props.trimEnd() + '\nSLACK_BOT_TOKEN=' + slackConfig.botToken + '\n';
94
+ }
95
+ if (slackConfig.appToken) {
96
+ const replaced = props.replace(/^#?\s*SLACK_APP_TOKEN=.*/m, 'SLACK_APP_TOKEN=' + slackConfig.appToken);
97
+ props = replaced !== props ? replaced : props.trimEnd() + '\nSLACK_APP_TOKEN=' + slackConfig.appToken + '\n';
98
+ }
99
+ fs.writeFileSync(workspaceProps, props);
100
+ }
101
+ fs.writeFileSync(path.join(workspace, 'startAll.sh'), `#!/usr/bin/env bash
102
+ # Start all valid Sentinel project instances.
103
+ # A valid project must have config/repo-configs; do
104
+ name=$(basename "$project_dir")
105
+ [[ "$name" == "code" ]] && continue
106
+ # Auto-generate start.sh / stop.sh if missing (codeDir = $WORKSPACE/code)
107
+ if [[ ! -f "$project_dir/start.sh" ]]; then
108
+ code_dir="$WORKSPACE/code"
109
+ python_bin="$code_dir/.venv/bin/python3"
110
+ sed -e "s|__NAME__|$name|g" -e "s|__CODE_DIR__|$code_dir|g" -e "s|__PYTHON_BIN__|$python_bin|g" << 'STARTSH' > "$project_dir/start.sh"
111
+ #!/usr/bin/env bash
112
+ set -euo pipefail
113
+ DIR="$(cd "$(dirname "$0")" && pwd)"
114
+ PID_FILE="$DIR/sentinel.pid"
115
+ if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
116
+ echo "[sentinel] __NAME__ already running (PID $(cat "$PID_FILE"))"
117
+ exit 0
118
+ fi
119
+ pkill -f "sentinel.main --config ${DIR}/config" 2>/dev/null || true
120
+ rm -f "$PID_FILE"
121
+ AUTH_OUT=$(claude --print \"hi\" 2>&1 || true)
122
+ if echo "$AUTH_OUT" | grep -Eqi "not logged in|/login"; then
123
+ echo "[sentinel] Claude Code is not authenticated. Run: claude then /login"
124
+ exit 1
125
+ fi
126
+ mkdir -p "$DIR/logs" "$DIR/workspace/fetched" "$DIR/workspace/patches" "$DIR/issues"
127
+ cd "$DIR"
128
+ PYTHONPATH="__CODE_DIR__" "__PYTHON_BIN__" -m sentinel.main --config ./config \
129
+ >> "$DIR/logs/sentinel.log" 2>&1 &
130
+ echo $! > "$PID_FILE"
131
+ echo "[sentinel] __NAME__ started (PID $!)"
132
+ STARTSH
133
+ chmod +x "$project_dir/start.sh"
134
+ echo "[sentinel] Auto-generated start.sh for $name"
135
+ fi
136
+ if [[ ! -f "$project_dir/stop.sh" ]]; then
137
+ sed -e "s|__NAME__|$name|g" << 'STOPSH' > "$project_dir/stop.sh"
138
+ #!/usr/bin/env bash
139
+ set -euo pipefail
140
+ DIR="$(cd "$(dirname "$0")" && pwd)"
141
+ PID_FILE="$DIR/sentinel.pid"
142
+ if [[ ! -f "$PID_FILE" ]]; then
143
+ echo "[sentinel] __NAME__ — no PID file, not running"
144
+ exit 0
145
+ fi
146
+ PID=$(cat "$PID_FILE")
147
+ if kill -0 "$PID" 2>/dev/null; then
148
+ kill "$PID"
149
+ echo "[sentinel] __NAME__ stopped (PID $PID)"
150
+ else
151
+ echo "[sentinel] __NAME__ — PID $PID not running"
152
+ fi
153
+ rm -f "$PID_FILE"
154
+ STOPSH
155
+ chmod +x "$project_dir/stop.sh"
156
+ echo "[sentinel] Auto-generated stop.sh for $name"
157
+ fi
158
+ # Must have at least one repo-config with a valid GitHub REPO_URL
159
+ repo_configs_dir="$project_dir/config/repo-configs"
160
+ if [[ ! -d "$repo_configs_dir" ]]; then
161
+ echo "[sentinel] Skipping $name — config/repo-configs/ directory not found"
162
+ skipped=$((skipped + 1))
163
+ continue
164
+ fi
165
+ has_config=false
166
+ valid_repo=false
167
+ for props in "$repo_configs_dir/"*.properties; do
168
+ [[ -f "$props" ]] || continue
169
+ [[ "$(basename "$props")" == _* ]] && continue
170
+ has_config=true
171
+ if grep -qE "^REPO_URL[[:space:]]*=[[:space:]]*(git@github\.com:|https://github\.com/)" "$props"; then
172
+ valid_repo=true
173
+ break
174
+ else
175
+ repo_url=$(grep -E "^REPO_URL[[:space:]]*=" "$props" | head -1 | cut -d= -f2- | xargs 2>/dev/null || true)
176
+ if [[ -z "$repo_url" ]]; then
177
+ echo "[sentinel] Skipping $name — REPO_URL not set in $(basename \"$props\")"
178
+ else
179
+ echo "[sentinel] Skipping $name — REPO_URL in $(basename \"$props\") is not a GitHub URL: $repo_url"
180
+ fi
181
+ fi
182
+ done
183
+ if [[ "$has_config" == "false" ]]; then
184
+ echo "[sentinel] Skipping $name — no .properties files in config/repo-configs/ (only _example?)"
185
+ skipped=$((skipped + 1))
186
+ continue
187
+ fi
188
+ if [[ "$valid_repo" == "false" ]]; then
189
+ skipped=$((skipped + 1))
190
+ continue
191
+ fi
192
+ bash "$project_dir/start.sh"
193
+ started=$((started + 1))
194
+ done
195
+ echo "[sentinel] $started project(s) started, $skipped skipped"
196
+ `, { mode: 0o755 });
197
+ // stopAll.sh
198
+ fs.writeFileSync(path.join(workspace, 'stopAll.sh'), `#!/usr/bin/env bash
199
+ # Stop all Sentinel project instances
200
+ WORKSPACE="$(cd "$(dirname "$0")" && pwd)"
201
+ stopped=0
202
+ for project_dir in "$WORKSPACE"/*/; do
203
+ name=$(basename "$project_dir")
204
+ [[ "$name" == "code" ]] && continue
205
+ [[ -f "$project_dir/stop.sh" ]] || continue
206
+ bash "$project_dir/stop.sh"
207
+ stopped=$((stopped + 1))
208
+ done
209
+ echo "[sentinel] $stopped project(s) stopped"
210
+ `, { mode: 0o755 });
211
+ }
212
+ module.exports = { writeExampleProject, generateProjectScripts, generateWorkspaceScripts };
@@ -0,0 +1,58 @@
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
+ });
package/lib/generate.js CHANGED
@@ -46,7 +46,7 @@ if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
46
46
  fi
47
47
 
48
48
  # Kill any orphaned sentinel processes for this project (stale PIDs not in PID file)
49
- pkill -f "sentinel.main --config ${DIR}/config" 2>/dev/null || true
49
+ pkill -f "sentinel.main --config $DIR/config" 2>/dev/null || true
50
50
  rm -f "$PID_FILE"
51
51
 
52
52
  # Check Claude Code authentication
@@ -144,7 +144,7 @@ if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
144
144
  echo "[sentinel] __NAME__ already running (PID $(cat "$PID_FILE"))"
145
145
  exit 0
146
146
  fi
147
- pkill -f "sentinel.main --config ${DIR}/config" 2>/dev/null || true
147
+ pkill -f "sentinel.main --config $DIR/config" 2>/dev/null || true
148
148
  rm -f "$PID_FILE"
149
149
  AUTH_OUT=$(claude --print \"hi\" 2>&1 || true)
150
150
  if echo "$AUTH_OUT" | grep -Eqi "not logged in|/login"; then
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.0.61",
3
+ "version": "1.0.63",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -138,7 +138,8 @@ Don't pad responses. Don't say "Great question!" or "Certainly!".
138
138
  If you don't know something, use a tool to find out before saying you don't know.
139
139
 
140
140
  When the engineer's request is fully handled, end your LAST message with the token: [DONE]
141
- Always write at least one sentence of actual reply before [DONE] never send [DONE] alone.
141
+ 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.
142
+ For greetings like "hello" or empty messages, introduce yourself briefly and offer help, then end with [DONE].
142
143
  If you need a follow-up from them, do NOT include [DONE] — wait for their next message.
143
144
  """
144
145
 
@@ -1003,7 +1004,7 @@ async def _handle_with_cli(
1003
1004
  is_done = "[DONE]" in reply
1004
1005
  reply = reply.replace("[DONE]", "").strip()
1005
1006
  if not reply:
1006
- reply = "Done."
1007
+ reply = "Hi! I'm Sentinel, your autonomous DevOps agent. How can I help you?"
1007
1008
 
1008
1009
  history.append({"role": "user", "content": message})
1009
1010
  history.append({"role": "assistant", "content": reply})
@@ -1089,7 +1090,7 @@ async def handle_message(
1089
1090
  is_done = "[DONE]" in reply
1090
1091
  reply = reply.replace("[DONE]", "").strip()
1091
1092
  if not reply:
1092
- reply = "Done."
1093
+ reply = "Hi! I'm Sentinel, your autonomous DevOps agent. How can I help you?"
1093
1094
  history.append({"role": "assistant", "content": response.content})
1094
1095
  return reply, is_done
1095
1096