@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.
package/.cairn/minify-map.json
CHANGED
|
@@ -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": "
|
|
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 $
|
|
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 $
|
|
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
|
@@ -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
|
|
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 = "
|
|
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 = "
|
|
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
|
|