@misterhuydo/sentinel 1.0.86 → 1.0.88

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/.hint-lock CHANGED
@@ -1 +1 @@
1
- 2026-03-23T05:33:10.957Z
1
+ 2026-03-23T06:03:48.001Z
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-03-23T05:31:41.079Z",
3
- "checkpoint_at": "2026-03-23T05:31:41.080Z",
2
+ "message": "Auto-checkpoint at 2026-03-23T06:00:14.460Z",
3
+ "checkpoint_at": "2026-03-23T06:00:14.461Z",
4
4
  "active_files": [],
5
5
  "notes": [],
6
6
  "mtime_snapshot": {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.0.86",
3
+ "version": "1.0.88",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -113,6 +113,10 @@ What you can do (tools available):
113
113
  21. tail_log — Fetch the last N lines of a log source live, without a grep filter.
114
114
  e.g. "show recent SSOLWA logs", "tail STS", "last 200 lines from 1881 logs"
115
115
 
116
+ 22. post_file — Upload a text file to the Slack conversation (diff, log excerpt, report, CSV).
117
+ Use when output is too large for chat, or the user asks to download/export something.
118
+ e.g. "give me that as a file", "export the log", "send me the diff"
119
+
116
120
  When someone asks what you can do, what you support, what your capabilities are, or how you can help,
117
121
  reply with a short summary grouped by category:
118
122
 
@@ -154,6 +158,9 @@ reply with a short summary grouped by category:
154
158
  *Self-management*
155
159
  • `upgrade_sentinel` — git pull + pip install + restart — "upgrade sentinel", "update yourself"
156
160
 
161
+ *File sharing*
162
+ • `post_file` — upload a file to Slack — "give me that as a file", "export the log", "send me the diff"
163
+
157
164
  Tone: direct, professional, like a senior engineer who owns the system.
158
165
  Don't pad responses. Don't say "Great question!" or "Certainly!".
159
166
  If you don't know something, use a tool to find out before saying you don't know.
@@ -551,6 +558,34 @@ _TOOLS = [
551
558
  "required": ["source"],
552
559
  },
553
560
  },
561
+ {
562
+ "name": "post_file",
563
+ "description": (
564
+ "Upload a text file directly to the Slack conversation so the user can read or download it. "
565
+ "Use when: output is too large for a chat message, the user asks to 'download', 'export', or "
566
+ "'send as a file', or when formatted content (diffs, logs, CSVs, reports) is clearer as a file. "
567
+ "e.g. 'give me that as a file', 'export the log', 'send me the diff for PR #41', "
568
+ "'download the health report', 'export recent errors as CSV'"
569
+ ),
570
+ "input_schema": {
571
+ "type": "object",
572
+ "properties": {
573
+ "content": {
574
+ "type": "string",
575
+ "description": "The full text content of the file to upload",
576
+ },
577
+ "filename": {
578
+ "type": "string",
579
+ "description": "Filename with extension, e.g. 'fix-ab12.diff', 'sentinel-report.txt', 'errors.csv', 'ssolwa.log'",
580
+ },
581
+ "title": {
582
+ "type": "string",
583
+ "description": "Optional display title shown above the file in Slack (defaults to filename)",
584
+ },
585
+ },
586
+ "required": ["content", "filename"],
587
+ },
588
+ },
554
589
  ]
555
590
 
556
591
 
@@ -617,7 +652,7 @@ def _git_pull(path: Path) -> dict:
617
652
 
618
653
  # ── Tool execution ────────────────────────────────────────────────────────────
619
654
 
620
- async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=None, user_id: str = "") -> str:
655
+ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=None, user_id: str = "", channel: str = "") -> str:
621
656
  if name == "get_status":
622
657
  hours = int(inputs.get("hours", 24))
623
658
  errors = store.get_recent_errors(hours)
@@ -1206,6 +1241,27 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
1206
1241
  results.append({"source": props.stem, "error": str(e)})
1207
1242
  return json.dumps({"results": results})
1208
1243
 
1244
+ if name == "post_file":
1245
+ if not slack_client or not channel:
1246
+ return json.dumps({"error": "No Slack channel context — cannot upload file"})
1247
+ content = inputs.get("content", "")
1248
+ filename = inputs.get("filename", "sentinel-output.txt")
1249
+ title = inputs.get("title", filename)
1250
+ if not content:
1251
+ return json.dumps({"error": "No content provided"})
1252
+ try:
1253
+ await slack_client.files_upload_v2(
1254
+ channel=channel,
1255
+ content=content,
1256
+ filename=filename,
1257
+ title=title,
1258
+ )
1259
+ logger.info("Boss post_file: uploaded %s (%d bytes) to %s", filename, len(content), channel)
1260
+ return json.dumps({"ok": True, "filename": filename, "bytes": len(content)})
1261
+ except Exception as e:
1262
+ logger.warning("Boss post_file failed: %s", e)
1263
+ return json.dumps({"error": str(e)})
1264
+
1209
1265
  if name == "my_stats":
1210
1266
  hours = int(inputs.get("hours", 168))
1211
1267
  errors = store.get_recent_errors(hours)
@@ -1379,9 +1435,12 @@ async def _handle_with_cli(
1379
1435
  )
1380
1436
  history_text += f"\n{role}: {content}"
1381
1437
 
1438
+ slack_mention = f"<@{user_id}>" if user_id else (user_name or "")
1382
1439
  prompt = (
1383
1440
  _SYSTEM
1384
- + (f"\nYou are speaking with: {user_name}" if user_name else "")
1441
+ + (f"\nYou are speaking with: {user_name} (Slack mention: {slack_mention})" if user_name else "")
1442
+ + "\nAlways start your reply by addressing the user directly using their Slack mention, e.g. \"<@U123> here is what I found...\"."
1443
+ + " Never use their plain name — always use the <@USER_ID> format so Slack highlights it."
1385
1444
  + f"\n\nCurrent time: {ts}"
1386
1445
  + f"\nSentinel status: {'⏸ PAUSED' if paused else '▶ RUNNING'}"
1387
1446
  + f"\nManaged repos: {', '.join(repos) if repos else '(none configured)'}"
@@ -1464,6 +1523,7 @@ async def _handle_with_api(
1464
1523
  user_name: str = "",
1465
1524
  user_id: str = "",
1466
1525
  attachments: list | None = None,
1526
+ channel: str = "",
1467
1527
  ) -> tuple[str, bool]:
1468
1528
  import anthropic
1469
1529
 
@@ -1475,9 +1535,12 @@ async def _handle_with_api(
1475
1535
  ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
1476
1536
  known_projects = [_read_project_name(d) for d in _find_project_dirs()]
1477
1537
  log_sources = list(cfg_loader.log_sources.keys())
1538
+ slack_mention = f"<@{user_id}>" if user_id else (user_name or "")
1478
1539
  system = (
1479
1540
  _SYSTEM
1480
- + (f"\nYou are speaking with: {user_name}" if user_name else "")
1541
+ + (f"\nYou are speaking with: {user_name} (Slack mention: {slack_mention})" if user_name else "")
1542
+ + "\nAlways start your reply by addressing the user directly using their Slack mention, e.g. \"<@U123> here is what I found...\"."
1543
+ + " Never use their plain name — always use the <@USER_ID> format so Slack highlights it."
1481
1544
  + f"\n\nCurrent time: {ts}"
1482
1545
  + f"\nSentinel status: {'⏸ PAUSED' if paused else '▶ RUNNING'}"
1483
1546
  + f"\nManaged repos: {', '.join(repos) if repos else '(none configured)'}"
@@ -1524,7 +1587,7 @@ async def _handle_with_api(
1524
1587
  messages.append({"role": "assistant", "content": response.content})
1525
1588
  tool_results = []
1526
1589
  for tc in tool_blocks:
1527
- result = await _run_tool(tc.name, tc.input, cfg_loader, store, slack_client=slack_client, user_id=user_id)
1590
+ result = await _run_tool(tc.name, tc.input, cfg_loader, store, slack_client=slack_client, user_id=user_id, channel=channel)
1528
1591
  logger.info("Boss tool: %s(%s) → %s", tc.name, tc.input, result[:120])
1529
1592
  tool_results.append({
1530
1593
  "type": "tool_result",
@@ -1545,6 +1608,7 @@ async def handle_message(
1545
1608
  user_name: str = "",
1546
1609
  user_id: str = "",
1547
1610
  attachments: list | None = None,
1611
+ channel: str = "",
1548
1612
  ) -> tuple[str, bool]:
1549
1613
  """
1550
1614
  Process one user message through the Sentinel Boss (Claude with tool use).
@@ -1566,7 +1630,7 @@ async def handle_message(
1566
1630
  import anthropic # noqa: F401
1567
1631
  return await _handle_with_api(
1568
1632
  message, history, cfg_loader, store, slack_client=slack_client,
1569
- user_name=user_name, user_id=user_id, attachments=attachments,
1633
+ user_name=user_name, user_id=user_id, attachments=attachments, channel=channel,
1570
1634
  )
1571
1635
  except Exception as api_err:
1572
1636
  err_str = str(api_err)
@@ -389,6 +389,7 @@ async def _run_turn(session: _Session, message: str, client, cfg_loader, store,
389
389
  user_name=session.user_name,
390
390
  user_id=session.user_id,
391
391
  attachments=attachments or [],
392
+ channel=channel,
392
393
  )
393
394
  except Exception as e:
394
395
  logger.exception("Sentinel Boss error: %s", e)