@misterhuydo/sentinel 1.5.51 → 1.5.52

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.5.51",
3
+ "version": "1.5.52",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -1 +1 @@
1
- __version__ = "1.5.51"
1
+ __version__ = "1.5.52"
@@ -5192,6 +5192,21 @@ async def _handle_with_api(
5192
5192
  else:
5193
5193
  user_time_hint = ""
5194
5194
  _known_projects = [_read_project_name(d) for d in _find_project_dirs()]
5195
+
5196
+ # Build alias map: repo → [alias, alias, ...] from config + DB
5197
+ _alias_lines: list[str] = []
5198
+ for _rname, _rcfg in cfg_loader.repos.items():
5199
+ _aliases = list(getattr(_rcfg, "service_aliases", []))
5200
+ # also pull DB-stored aliases for this repo
5201
+ _db_aliases = [r["service_name"] for r in (store.get_all_service_aliases() if hasattr(store, "get_all_service_aliases") else []) if r.get("repo_name") == _rname]
5202
+ _all = list(dict.fromkeys(_aliases + _db_aliases)) # dedupe, preserve order
5203
+ if _all:
5204
+ _alias_lines.append(f" {_rname} = {', '.join(_all)}")
5205
+ _aliases_block = (
5206
+ "\nService alias map (short names → repo):\n" + "\n".join(_alias_lines)
5207
+ if _alias_lines else ""
5208
+ )
5209
+
5195
5210
  system = (
5196
5211
  _resolve_system(
5197
5212
  boss_mode=getattr(cfg_loader.sentinel, "boss_mode", "standard"),
@@ -5207,6 +5222,12 @@ async def _handle_with_api(
5207
5222
  + (f"\n{user_time_hint}" if user_time_hint else "")
5208
5223
  + f"\nSentinel status: {'⏸ PAUSED' if paused else '▶ RUNNING'}"
5209
5224
  + f"\nManaged repos: {', '.join(repos) if repos else '(none configured)'}"
5225
+ + _aliases_block
5226
+ + "\nUNKNOWN SERVICE NAME RULE: If the user mentions a service/repo shorthand that is NOT"
5227
+ + " in 'Managed repos' and NOT in the alias map above, ask EXACTLY: \"What is <name>?\""
5228
+ + " before doing anything else. When the user answers with a repo name, immediately call"
5229
+ + " save_service_alias(service_name=<name>, repo_name=<answer>), then proceed with the"
5230
+ + " original request. Never guess or assume — always ask and save."
5210
5231
  + (f"\nLog sources: {', '.join(log_sources)}" if log_sources else "")
5211
5232
  + (f"\nKnown projects in workspace: {', '.join(known_projects)}" if known_projects else "")
5212
5233
  + f"\nAdmin access for this user: {'YES — admin tools are available' if is_admin else 'NO — admin tools will be refused'}"
@@ -181,16 +181,33 @@ import uuid as _uuid
181
181
  from pathlib import Path as _Path
182
182
 
183
183
 
184
- def _infer_target_repo(text: str, repos: dict, store=None) -> str:
184
+ def _infer_target_repo(text: str, repos: dict, store=None, bot_name: str = "") -> str:
185
185
  """
186
186
  Try to infer a target repo from bot message content.
187
- Checks in order:
188
- 1. SERVICE_ALIASES declared in repo config files (e.g. SERVICE_ALIASES=STS,SecurityTokenService)
189
- 2. Saved aliases in DB (learned at runtime via Boss conversation)
190
- 3. Case-insensitive substring match against repo names
191
- Returns the repo name on a unique match, empty string otherwise.
187
+ Strategy:
188
+ 1. Full-text scan: check every repo's SERVICE_ALIASES as whole-word matches in text.
189
+ If exactly one repo matches use it. Avoids needing a specific "Service: <name>" pattern.
190
+ 2. Structured hints (Service:/Class: patterns) config aliases → DB aliases → substring.
191
+ 3. DB aliases keyed on bot_name (for bots registered via save_service_alias).
192
+ Returns the repo name on an unambiguous match, empty string otherwise.
192
193
  """
193
194
  import re as _re
195
+
196
+ text_lower = text.lower()
197
+
198
+ # 1. Full-text scan of all config-declared SERVICE_ALIASES
199
+ matched_repos: set[str] = set()
200
+ for repo_name, repo in repos.items():
201
+ declared = getattr(repo, "service_aliases", [])
202
+ for alias in declared:
203
+ # whole-word match (handles "STS", "SSOLWA", "SecurityTokenService", etc.)
204
+ if _re.search(r'(?i)\b' + _re.escape(alias) + r'\b', text):
205
+ matched_repos.add(repo_name)
206
+ break
207
+ if len(matched_repos) == 1:
208
+ return next(iter(matched_repos))
209
+
210
+ # 2. Structured hints: "Service: X" or "Class: X" patterns
194
211
  hints = _re.findall(r'(?i)service:\s*(\S+)', text)
195
212
  hints += _re.findall(r'(?i)class:\s*(\w+)', text)
196
213
 
@@ -198,23 +215,29 @@ def _infer_target_repo(text: str, repos: dict, store=None) -> str:
198
215
  hint_clean = hint.rstrip(".,:")
199
216
  hint_lower = hint_clean.lower()
200
217
 
201
- # 1. Config-declared SERVICE_ALIASES (authoritative, version-controlled)
218
+ # Config aliases (already checked above but try exact match here too)
202
219
  for repo_name, repo in repos.items():
203
220
  declared = getattr(repo, "service_aliases", [])
204
221
  if any(hint_lower == a.lower() for a in declared):
205
222
  return repo_name
206
223
 
207
- # 2. Runtime DB aliases (learned via Boss conversation)
224
+ # DB aliases
208
225
  if store:
209
226
  alias = store.get_service_alias(hint_clean)
210
227
  if alias and alias in repos:
211
228
  return alias
212
229
 
213
- # 3. Case-insensitive substring match against repo names
230
+ # Substring match against repo names
214
231
  sub_matches = [r for r in repos if hint_lower in r.lower()]
215
232
  if len(sub_matches) == 1:
216
233
  return sub_matches[0]
217
234
 
235
+ # 3. DB alias keyed on bot_name (for bots explicitly mapped via save_service_alias)
236
+ if bot_name and store:
237
+ alias = store.get_service_alias(bot_name)
238
+ if alias and alias in repos:
239
+ return alias
240
+
218
241
  return ""
219
242
 
220
243
 
@@ -261,35 +284,36 @@ async def _handle_bot_message(event: dict, client, cfg_loader, store) -> None:
261
284
  target_repo = (bot_info or {}).get("target_repo") or ""
262
285
 
263
286
  # Auto-detect target_repo from message content when not pre-configured
287
+ bot_name = (bot_info or {}).get("bot_name") or bot_id
264
288
  if not target_repo and hasattr(cfg_loader, "repos") and cfg_loader.repos:
265
- target_repo = _infer_target_repo(text, cfg_loader.repos, store=store)
289
+ target_repo = _infer_target_repo(text, cfg_loader.repos, store=store, bot_name=bot_name)
266
290
  if not target_repo and len(cfg_loader.repos) > 1:
267
291
  # Can't determine — ask in the channel and store as pending routing
268
- bot_name = (bot_info or {}).get("bot_name") or bot_id
269
292
  service_hint = _extract_service_hint(text)
270
- hint_label = f"`{service_hint}`" if service_hint else "this alert"
271
- repo_names = ", ".join(f"`{r}`" for r in sorted(cfg_loader.repos))
293
+ # Use bot_name as the routing key when no service hint found in message
294
+ routing_key = service_hint or bot_name
295
+ if service_hint:
296
+ question = f":mag: Got an alert from *{bot_name}* — what repo is `{service_hint}`?"
297
+ else:
298
+ preview = text[:120].replace("\n", " ")
299
+ question = f":mag: Got an alert from *{bot_name}* — which repo does this belong to?\n_{preview}_"
272
300
  try:
273
- await client.chat_postMessage(
274
- channel=channel,
275
- text=f":mag: Got an alert from *{bot_name}* — what repo is `{service_hint}`?",
276
- )
301
+ await client.chat_postMessage(channel=channel, text=question)
277
302
  except Exception as _e:
278
303
  logger.warning("Bot watcher: could not post routing question: %s", _e)
279
304
  # Store as pending so Boss can reprocess when user replies
280
- if service_hint:
281
- try:
282
- default_issues = str(_Path(cfg_loader.sentinel.workspace_dir).parent / "issues")
283
- store.add_pending_routing(
284
- service_hint=service_hint,
285
- bot_id=bot_id,
286
- channel=channel,
287
- ts=ts,
288
- content=text,
289
- issues_dir=default_issues,
290
- )
291
- except Exception as _e:
292
- logger.warning("Bot watcher: could not store pending routing: %s", _e)
305
+ try:
306
+ default_issues = str(_Path(cfg_loader.sentinel.workspace_dir).parent / "issues")
307
+ store.add_pending_routing(
308
+ service_hint=routing_key,
309
+ bot_id=bot_id,
310
+ channel=channel,
311
+ ts=ts,
312
+ content=text,
313
+ issues_dir=default_issues,
314
+ )
315
+ except Exception as _e:
316
+ logger.warning("Bot watcher: could not store pending routing: %s", _e)
293
317
  return # drop this message until routing is resolved
294
318
 
295
319
  # Resolve the project issues directory