@misterhuydo/sentinel 1.0.88 → 1.0.90

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-23T06:00:14.460Z",
3
- "checkpoint_at": "2026-03-23T06:00:14.461Z",
2
+ "message": "Auto-checkpoint at 2026-03-23T06:09:59.037Z",
3
+ "checkpoint_at": "2026-03-23T06:09:59.038Z",
4
4
  "active_files": [],
5
5
  "notes": [],
6
6
  "mtime_snapshot": {}
package/lib/generate.js CHANGED
@@ -112,13 +112,17 @@ function generateWorkspaceScripts(workspace, smtpConfig = {}, slackConfig = {},
112
112
  if (authConfig.apiKey || authConfig.claudeProForTasks !== undefined) {
113
113
  let props = fs.readFileSync(workspaceProps, 'utf8');
114
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';
115
+ if (/^#?\s*ANTHROPIC_API_KEY=/m.test(props))
116
+ props = props.replace(/^#?\s*ANTHROPIC_API_KEY=.*/mg, 'ANTHROPIC_API_KEY=' + authConfig.apiKey);
117
+ else
118
+ props = props.trimEnd() + '\nANTHROPIC_API_KEY=' + authConfig.apiKey + '\n';
117
119
  }
118
120
  if (authConfig.claudeProForTasks !== undefined) {
119
121
  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
+ if (/^#?\s*CLAUDE_PRO_FOR_TASKS=/m.test(props))
123
+ props = props.replace(/^#?\s*CLAUDE_PRO_FOR_TASKS=.*/mg, 'CLAUDE_PRO_FOR_TASKS=' + val);
124
+ else
125
+ props = props.trimEnd() + '\nCLAUDE_PRO_FOR_TASKS=' + val + '\n';
122
126
  }
123
127
  fs.writeFileSync(workspaceProps, props);
124
128
  }
@@ -126,12 +130,16 @@ function generateWorkspaceScripts(workspace, smtpConfig = {}, slackConfig = {},
126
130
  if (slackConfig.botToken || slackConfig.appToken) {
127
131
  let props = fs.readFileSync(workspaceProps, 'utf8');
128
132
  if (slackConfig.botToken) {
129
- const replaced = props.replace(/^#?\s*SLACK_BOT_TOKEN=.*/m, 'SLACK_BOT_TOKEN=' + slackConfig.botToken);
130
- props = replaced !== props ? replaced : props.trimEnd() + '\nSLACK_BOT_TOKEN=' + slackConfig.botToken + '\n';
133
+ if (/^#?\s*SLACK_BOT_TOKEN=/m.test(props))
134
+ props = props.replace(/^#?\s*SLACK_BOT_TOKEN=.*/mg, 'SLACK_BOT_TOKEN=' + slackConfig.botToken);
135
+ else
136
+ props = props.trimEnd() + '\nSLACK_BOT_TOKEN=' + slackConfig.botToken + '\n';
131
137
  }
132
138
  if (slackConfig.appToken) {
133
- const replaced = props.replace(/^#?\s*SLACK_APP_TOKEN=.*/m, 'SLACK_APP_TOKEN=' + slackConfig.appToken);
134
- props = replaced !== props ? replaced : props.trimEnd() + '\nSLACK_APP_TOKEN=' + slackConfig.appToken + '\n';
139
+ if (/^#?\s*SLACK_APP_TOKEN=/m.test(props))
140
+ props = props.replace(/^#?\s*SLACK_APP_TOKEN=.*/mg, 'SLACK_APP_TOKEN=' + slackConfig.appToken);
141
+ else
142
+ props = props.trimEnd() + '\nSLACK_APP_TOKEN=' + slackConfig.appToken + '\n';
135
143
  }
136
144
  fs.writeFileSync(workspaceProps, props);
137
145
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.0.88",
3
+ "version": "1.0.90",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -1436,11 +1436,14 @@ async def _handle_with_cli(
1436
1436
  history_text += f"\n{role}: {content}"
1437
1437
 
1438
1438
  slack_mention = f"<@{user_id}>" if user_id else (user_name or "")
1439
+ known_users = store.get_all_users()
1440
+ users_hint = ", ".join(f"<@{uid}> = {name}" for uid, name in known_users.items())
1439
1441
  prompt = (
1440
1442
  _SYSTEM
1441
1443
  + (f"\nYou are speaking with: {user_name} (Slack mention: {slack_mention})" if user_name else "")
1442
1444
  + "\nAlways start your reply by addressing the user directly using their Slack mention, e.g. \"<@U123> here is what I found...\"."
1443
1445
  + " Never use their plain name — always use the <@USER_ID> format so Slack highlights it."
1446
+ + (f"\nKnown Slack users: {users_hint}" if users_hint else "")
1444
1447
  + f"\n\nCurrent time: {ts}"
1445
1448
  + f"\nSentinel status: {'⏸ PAUSED' if paused else '▶ RUNNING'}"
1446
1449
  + f"\nManaged repos: {', '.join(repos) if repos else '(none configured)'}"
@@ -1536,11 +1539,14 @@ async def _handle_with_api(
1536
1539
  known_projects = [_read_project_name(d) for d in _find_project_dirs()]
1537
1540
  log_sources = list(cfg_loader.log_sources.keys())
1538
1541
  slack_mention = f"<@{user_id}>" if user_id else (user_name or "")
1542
+ known_users = store.get_all_users() # {user_id: display_name}
1543
+ users_hint = ", ".join(f"<@{uid}> = {name}" for uid, name in known_users.items())
1539
1544
  system = (
1540
1545
  _SYSTEM
1541
1546
  + (f"\nYou are speaking with: {user_name} (Slack mention: {slack_mention})" if user_name else "")
1542
1547
  + "\nAlways start your reply by addressing the user directly using their Slack mention, e.g. \"<@U123> here is what I found...\"."
1543
1548
  + " Never use their plain name — always use the <@USER_ID> format so Slack highlights it."
1549
+ + (f"\nKnown Slack users: {users_hint}" if users_hint else "")
1544
1550
  + f"\n\nCurrent time: {ts}"
1545
1551
  + f"\nSentinel status: {'⏸ PAUSED' if paused else '▶ RUNNING'}"
1546
1552
  + f"\nManaged repos: {', '.join(repos) if repos else '(none configured)'}"
@@ -342,6 +342,7 @@ async def _dispatch(event: dict, client, cfg_loader, store) -> None:
342
342
  return
343
343
 
344
344
  user_name = await _resolve_name(client, user_id)
345
+ store.upsert_user(user_id, user_name)
345
346
  session = await _get_or_create_session(user_id, user_name, channel)
346
347
 
347
348
  if session.busy:
@@ -391,6 +391,43 @@ class StateStore:
391
391
  ).fetchall()
392
392
  return [dict(r) for r in rows]
393
393
 
394
+ def upsert_user(self, user_id: str, display_name: str) -> None:
395
+ """Store or update the display name for a Slack user ID."""
396
+ with self._conn() as conn:
397
+ conn.execute(
398
+ "CREATE TABLE IF NOT EXISTS slack_users "
399
+ "(user_id TEXT PRIMARY KEY, display_name TEXT, updated_at TEXT)"
400
+ )
401
+ conn.execute(
402
+ "INSERT OR REPLACE INTO slack_users (user_id, display_name, updated_at) "
403
+ "VALUES (?, ?, ?)",
404
+ (user_id, display_name, _now()),
405
+ )
406
+
407
+ def get_user_name(self, user_id: str) -> str:
408
+ """Return the stored display name for a user ID, or the ID itself if unknown."""
409
+ with self._conn() as conn:
410
+ conn.execute(
411
+ "CREATE TABLE IF NOT EXISTS slack_users "
412
+ "(user_id TEXT PRIMARY KEY, display_name TEXT, updated_at TEXT)"
413
+ )
414
+ row = conn.execute(
415
+ "SELECT display_name FROM slack_users WHERE user_id = ?", (user_id,)
416
+ ).fetchone()
417
+ return row[0] if row else user_id
418
+
419
+ def get_all_users(self) -> dict[str, str]:
420
+ """Return all known {user_id: display_name} mappings."""
421
+ with self._conn() as conn:
422
+ conn.execute(
423
+ "CREATE TABLE IF NOT EXISTS slack_users "
424
+ "(user_id TEXT PRIMARY KEY, display_name TEXT, updated_at TEXT)"
425
+ )
426
+ rows = conn.execute(
427
+ "SELECT user_id, display_name FROM slack_users ORDER BY display_name"
428
+ ).fetchall()
429
+ return {r[0]: r[1] for r in rows}
430
+
394
431
  def save_conversation(self, user_id: str, history: list):
395
432
  """Persist the last N messages of a user conversation to SQLite."""
396
433
  trimmed = history[-self._MAX_HISTORY_MESSAGES:]