@integrity-labs/agt-cli 0.27.82 → 0.27.84

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.
@@ -14449,6 +14449,232 @@ function probeAgentSessionCached(codeName, ttlMs = SESSION_PROBE_TTL_MS, now = D
14449
14449
  return value;
14450
14450
  }
14451
14451
 
14452
+ // src/pane-tail.ts
14453
+ import { execFile } from "child_process";
14454
+ import { promisify } from "util";
14455
+ import { open, stat } from "fs/promises";
14456
+
14457
+ // src/channel-attachments.ts
14458
+ import { homedir } from "os";
14459
+ import { join as join2, resolve, sep } from "path";
14460
+ function resolveChannelInboundDir(codeName, channelSlug) {
14461
+ const base = join2(homedir(), ".augmented");
14462
+ const allowedSegment = /^[A-Za-z0-9_-]+$/;
14463
+ if (!allowedSegment.test(codeName) || !allowedSegment.test(channelSlug)) {
14464
+ throw new Error(
14465
+ `Refusing to resolve inbound dir \u2014 invalid codeName/channelSlug (got ${JSON.stringify({ codeName, channelSlug })})`
14466
+ );
14467
+ }
14468
+ const candidate = resolve(base, codeName, channelSlug);
14469
+ if (!isPathInside(candidate, base)) {
14470
+ throw new Error(`Refusing inbound dir outside ${base} (got ${candidate})`);
14471
+ }
14472
+ return candidate;
14473
+ }
14474
+ function classifyMimetype(mimetype) {
14475
+ if (typeof mimetype === "string" && mimetype.startsWith("image/")) return "image";
14476
+ return "attachment";
14477
+ }
14478
+ function buildSafeInboundPath(root, fileId, mimetype) {
14479
+ const safeId = fileId.replace(/[^A-Za-z0-9_-]/g, "");
14480
+ if (!safeId) throw new Error("Refusing to build inbound path for empty/invalid file id");
14481
+ const ext = extensionForMimetype(mimetype);
14482
+ const candidate = resolve(root, `${safeId}${ext}`);
14483
+ if (!isPathInside(candidate, root)) {
14484
+ throw new Error(`Refusing to build inbound path outside agent dir (got ${candidate})`);
14485
+ }
14486
+ return candidate;
14487
+ }
14488
+ function extensionForMimetype(mimetype) {
14489
+ if (!mimetype) return ".bin";
14490
+ switch (mimetype) {
14491
+ // Images
14492
+ case "image/png":
14493
+ return ".png";
14494
+ case "image/jpeg":
14495
+ case "image/jpg":
14496
+ return ".jpg";
14497
+ case "image/gif":
14498
+ return ".gif";
14499
+ case "image/webp":
14500
+ return ".webp";
14501
+ case "image/svg+xml":
14502
+ return ".svg";
14503
+ // Docs
14504
+ case "application/pdf":
14505
+ return ".pdf";
14506
+ case "text/plain":
14507
+ return ".txt";
14508
+ case "text/csv":
14509
+ return ".csv";
14510
+ case "application/json":
14511
+ return ".json";
14512
+ case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
14513
+ return ".docx";
14514
+ case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
14515
+ return ".xlsx";
14516
+ // Audio (Telegram voice notes are typically audio/ogg; regular
14517
+ // audio messages are audio/mpeg)
14518
+ case "audio/ogg":
14519
+ return ".ogg";
14520
+ case "audio/mpeg":
14521
+ return ".mp3";
14522
+ case "audio/mp4":
14523
+ return ".m4a";
14524
+ // Video
14525
+ case "video/mp4":
14526
+ return ".mp4";
14527
+ case "video/quicktime":
14528
+ return ".mov";
14529
+ default:
14530
+ return ".bin";
14531
+ }
14532
+ }
14533
+ function isPathInside(target, root) {
14534
+ const normalizedRoot = resolve(root) + sep;
14535
+ const normalizedTarget = resolve(target);
14536
+ return normalizedTarget === resolve(root) || normalizedTarget.startsWith(normalizedRoot);
14537
+ }
14538
+ function redactAugmentedPaths(msg) {
14539
+ const homePrefix = homedir().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14540
+ return msg.replaceAll(
14541
+ new RegExp(`${homePrefix}[\\\\/]\\.augmented(?:[\\\\/][^\\s'"\`]*)*`, "g"),
14542
+ "<augmented-path>"
14543
+ );
14544
+ }
14545
+
14546
+ // src/pane-tail.ts
14547
+ var execFileAsync = promisify(execFile);
14548
+ function evaluateDebugGate(opts) {
14549
+ if (!opts.channelId || !opts.channelId.startsWith("D")) {
14550
+ return { ok: false, reason: "not-dm" };
14551
+ }
14552
+ if (opts.allowedUsers.size === 0) {
14553
+ return { ok: false, reason: "allowlist-empty" };
14554
+ }
14555
+ if (!opts.userId || !opts.allowedUsers.has(opts.userId)) {
14556
+ return { ok: false, reason: "not-allowlisted" };
14557
+ }
14558
+ return { ok: true };
14559
+ }
14560
+ var PANE_LOG_TAIL_BYTES = 64 * 1024;
14561
+ var TMUX_CAPTURE_TIMEOUT_MS = 2e3;
14562
+ async function capturePaneSnapshot(opts) {
14563
+ const scrollback = opts.scrollbackLines ?? 200;
14564
+ try {
14565
+ const { stdout } = await execFileAsync(
14566
+ "tmux",
14567
+ ["capture-pane", "-p", "-t", agentTmuxSessionName(opts.codeName), "-S", `-${scrollback}`],
14568
+ { timeout: TMUX_CAPTURE_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024 }
14569
+ );
14570
+ return { source: "tmux", text: stdout };
14571
+ } catch {
14572
+ }
14573
+ if (!opts.agentDir) return null;
14574
+ const paneLogPath = `${opts.agentDir}/pane.log`;
14575
+ try {
14576
+ const tail = await readFileTail(paneLogPath, PANE_LOG_TAIL_BYTES);
14577
+ if (tail === null) return null;
14578
+ return { source: "pane-log", text: stripAnsi(tail) };
14579
+ } catch {
14580
+ return null;
14581
+ }
14582
+ }
14583
+ async function readFileTail(path, maxBytes) {
14584
+ const st = await stat(path);
14585
+ if (!st.isFile()) return null;
14586
+ const start = Math.max(0, st.size - maxBytes);
14587
+ const length = st.size - start;
14588
+ if (length === 0) return "";
14589
+ const handle = await open(path, "r");
14590
+ try {
14591
+ const buf = Buffer.alloc(length);
14592
+ await handle.read(buf, 0, length, start);
14593
+ let text = buf.toString("utf8");
14594
+ if (start > 0) {
14595
+ const nl = text.indexOf("\n");
14596
+ if (nl !== -1) text = text.slice(nl + 1);
14597
+ }
14598
+ return text;
14599
+ } finally {
14600
+ await handle.close();
14601
+ }
14602
+ }
14603
+ function stripAnsi(text) {
14604
+ return text.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-9;?<=>!]*[ -\/]*[@-~]/g, "").replace(/\x1b./g, "").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
14605
+ }
14606
+ var SECRET_PATTERNS = [
14607
+ // Augmented host API keys
14608
+ { re: /tlk_[A-Za-z0-9]{8,}/g, label: "agt-api-key" },
14609
+ // Slack tokens: bot/app/user/refresh/config
14610
+ { re: /x(?:ox[abprs]|app)-[A-Za-z0-9-]{8,}/g, label: "slack-token" },
14611
+ // JWTs (three base64url segments, first one always starts with eyJ)
14612
+ { re: /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, label: "jwt" },
14613
+ // OpenAI / Anthropic-style keys (sk-..., sk-ant-...)
14614
+ { re: /sk-[A-Za-z0-9_-]{16,}/g, label: "sk-key" },
14615
+ // AWS access key IDs
14616
+ { re: /(?:AKIA|ASIA)[0-9A-Z]{16}/g, label: "aws-key" },
14617
+ // GitHub tokens
14618
+ { re: /gh[pousr]_[A-Za-z0-9]{20,}/g, label: "github-token" }
14619
+ ];
14620
+ function redactPaneText(text) {
14621
+ let out = redactAugmentedPaths(text);
14622
+ for (const { re, label } of SECRET_PATTERNS) {
14623
+ out = out.replace(re, `<redacted:${label}>`);
14624
+ }
14625
+ return out;
14626
+ }
14627
+ var DEFAULT_MAX_CHARS = 3900;
14628
+ var REFLOW_COLS = 80;
14629
+ function formatPaneSnapshot(opts) {
14630
+ const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
14631
+ const header = opts.status.kind === "live" ? `\u{1F50E} Live pane tail for \`${opts.codeName}\` \u2014 captured ${opts.capturedAtLabel} \xB7 updates ~3s \xB7 expires in ${formatCountdown(opts.status.secondsRemaining)}` : `\u{1F50E} Pane tail for \`${opts.codeName}\` \u2014 captured ${opts.capturedAtLabel} \xB7 *expired* \u2014 run \`/debug\` to restart`;
14632
+ const fenceOverhead = header.length + "\n```\n".length + "\n```".length + 16;
14633
+ const contentBudget = Math.max(0, maxChars - fenceOverhead);
14634
+ const lines = reflowLines(sanitizeForCodeBlock(opts.text), REFLOW_COLS);
14635
+ const kept = [];
14636
+ let used = 0;
14637
+ for (let i = lines.length - 1; i >= 0; i--) {
14638
+ const cost = lines[i].length + 1;
14639
+ if (used + cost > contentBudget) break;
14640
+ kept.unshift(lines[i]);
14641
+ used += cost;
14642
+ }
14643
+ const truncated = kept.length < lines.length;
14644
+ const body = kept.join("\n").trimEnd();
14645
+ const block = body.length > 0 ? `\`\`\`
14646
+ ${truncated ? "\u2026\n" : ""}${body}
14647
+ \`\`\`` : "_(pane is empty)_";
14648
+ return `${header}
14649
+ ${block}`;
14650
+ }
14651
+ function formatCountdown(totalSeconds) {
14652
+ const s = Math.max(0, Math.floor(totalSeconds));
14653
+ return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
14654
+ }
14655
+ function sanitizeForCodeBlock(text) {
14656
+ return text.replace(/```/g, "`\u200B`\u200B`");
14657
+ }
14658
+ function reflowLines(text, cols) {
14659
+ const out = [];
14660
+ for (const rawLine of text.split("\n")) {
14661
+ const line = rawLine.replace(/\s+$/, "");
14662
+ if (line.length <= cols) {
14663
+ out.push(line);
14664
+ continue;
14665
+ }
14666
+ for (let i = 0; i < line.length; i += cols) {
14667
+ out.push(line.slice(i, i + cols));
14668
+ }
14669
+ }
14670
+ const collapsed = [];
14671
+ for (const line of out) {
14672
+ if (line === "" && collapsed[collapsed.length - 1] === "") continue;
14673
+ collapsed.push(line);
14674
+ }
14675
+ return collapsed;
14676
+ }
14677
+
14452
14678
  // src/slack-loop-throttle.ts
14453
14679
  var DEFAULT_THROTTLE = {
14454
14680
  threshold: 3,
@@ -14658,7 +14884,7 @@ var SLACK_EGRESS_TOOLS = /* @__PURE__ */ new Set([
14658
14884
 
14659
14885
  // src/slack-pending-inbound-cleanup.ts
14660
14886
  import { existsSync, readdirSync as readdirSync2, statSync, unlinkSync } from "fs";
14661
- import { join as join2 } from "path";
14887
+ import { join as join3 } from "path";
14662
14888
  function sanitizeMarkerSegment(value) {
14663
14889
  return value.replace(/[^A-Za-z0-9_-]/g, "_");
14664
14890
  }
@@ -14675,7 +14901,7 @@ function clearAllSlackPendingMarkersForThread(dir, channel, threadTs, clear = de
14675
14901
  try {
14676
14902
  for (const f of readdirSync2(dir)) {
14677
14903
  if (!f.startsWith(prefix) || !f.endsWith(".json")) continue;
14678
- clear(join2(dir, f));
14904
+ clear(join3(dir, f));
14679
14905
  cleared += 1;
14680
14906
  }
14681
14907
  } catch {
@@ -14690,7 +14916,7 @@ function clearSlackPendingMarkerByMessageTs(dir, channel, messageTs, clear = def
14690
14916
  try {
14691
14917
  for (const f of readdirSync2(dir)) {
14692
14918
  if (!f.startsWith(channelPrefix) || !f.endsWith(messageSuffix)) continue;
14693
- clear(join2(dir, f));
14919
+ clear(join3(dir, f));
14694
14920
  cleared += 1;
14695
14921
  }
14696
14922
  } catch {
@@ -14702,7 +14928,7 @@ function clearOldestSlackPendingMarkerInChannel(dir, channel, clear = defaultCle
14702
14928
  const channelPrefix = `${sanitizeMarkerSegment(channel)}__`;
14703
14929
  try {
14704
14930
  const entries = readdirSync2(dir).filter((f) => f.startsWith(channelPrefix) && f.endsWith(".json")).map((f) => {
14705
- const full = join2(dir, f);
14931
+ const full = join3(dir, f);
14706
14932
  let mtime = 0;
14707
14933
  try {
14708
14934
  mtime = statSync(full).mtimeMs;
@@ -14954,88 +15180,6 @@ async function runOrRetry(fn, opts) {
14954
15180
  }
14955
15181
  }
14956
15182
 
14957
- // src/channel-attachments.ts
14958
- import { homedir } from "os";
14959
- import { join as join3, resolve, sep } from "path";
14960
- function resolveChannelInboundDir(codeName, channelSlug) {
14961
- const base = join3(homedir(), ".augmented");
14962
- const allowedSegment = /^[A-Za-z0-9_-]+$/;
14963
- if (!allowedSegment.test(codeName) || !allowedSegment.test(channelSlug)) {
14964
- throw new Error(
14965
- `Refusing to resolve inbound dir \u2014 invalid codeName/channelSlug (got ${JSON.stringify({ codeName, channelSlug })})`
14966
- );
14967
- }
14968
- const candidate = resolve(base, codeName, channelSlug);
14969
- if (!isPathInside(candidate, base)) {
14970
- throw new Error(`Refusing inbound dir outside ${base} (got ${candidate})`);
14971
- }
14972
- return candidate;
14973
- }
14974
- function classifyMimetype(mimetype) {
14975
- if (typeof mimetype === "string" && mimetype.startsWith("image/")) return "image";
14976
- return "attachment";
14977
- }
14978
- function buildSafeInboundPath(root, fileId, mimetype) {
14979
- const safeId = fileId.replace(/[^A-Za-z0-9_-]/g, "");
14980
- if (!safeId) throw new Error("Refusing to build inbound path for empty/invalid file id");
14981
- const ext = extensionForMimetype(mimetype);
14982
- const candidate = resolve(root, `${safeId}${ext}`);
14983
- if (!isPathInside(candidate, root)) {
14984
- throw new Error(`Refusing to build inbound path outside agent dir (got ${candidate})`);
14985
- }
14986
- return candidate;
14987
- }
14988
- function extensionForMimetype(mimetype) {
14989
- if (!mimetype) return ".bin";
14990
- switch (mimetype) {
14991
- // Images
14992
- case "image/png":
14993
- return ".png";
14994
- case "image/jpeg":
14995
- case "image/jpg":
14996
- return ".jpg";
14997
- case "image/gif":
14998
- return ".gif";
14999
- case "image/webp":
15000
- return ".webp";
15001
- case "image/svg+xml":
15002
- return ".svg";
15003
- // Docs
15004
- case "application/pdf":
15005
- return ".pdf";
15006
- case "text/plain":
15007
- return ".txt";
15008
- case "text/csv":
15009
- return ".csv";
15010
- case "application/json":
15011
- return ".json";
15012
- case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
15013
- return ".docx";
15014
- case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
15015
- return ".xlsx";
15016
- // Audio (Telegram voice notes are typically audio/ogg; regular
15017
- // audio messages are audio/mpeg)
15018
- case "audio/ogg":
15019
- return ".ogg";
15020
- case "audio/mpeg":
15021
- return ".mp3";
15022
- case "audio/mp4":
15023
- return ".m4a";
15024
- // Video
15025
- case "video/mp4":
15026
- return ".mp4";
15027
- case "video/quicktime":
15028
- return ".mov";
15029
- default:
15030
- return ".bin";
15031
- }
15032
- }
15033
- function isPathInside(target, root) {
15034
- const normalizedRoot = resolve(root) + sep;
15035
- const normalizedTarget = resolve(target);
15036
- return normalizedTarget === resolve(root) || normalizedTarget.startsWith(normalizedRoot);
15037
- }
15038
-
15039
15183
  // src/slack-inbound-files.ts
15040
15184
  function classifySlackFile(file) {
15041
15185
  if (!file || typeof file.id !== "string" || !file.id) return null;
@@ -16346,7 +16490,8 @@ function buildSlackHelpMessage(codeName) {
16346
16490
  "\u2022 `/restart` \u2014 restart this agent",
16347
16491
  "\u2022 `/agent-status` \u2014 report whether this agent is online + last activity",
16348
16492
  "\u2022 `/kill` \u2014 silence all agents in this thread for 6h (use as a thread reply)",
16349
- "\u2022 `/unkill` \u2014 clear a kill (use as a thread reply)"
16493
+ "\u2022 `/unkill` \u2014 clear a kill (use as a thread reply)",
16494
+ "\u2022 `/debug` \u2014 live tail of this agent's terminal pane (DM only, allowlisted users; works while the channel process is alive \u2014 a wedged host still needs SSM diagnostics)"
16350
16495
  ].join("\n");
16351
16496
  }
16352
16497
  var lastActivityAt = null;
@@ -16435,6 +16580,193 @@ async function postEphemeralViaResponseUrl(responseUrl, text, logTag) {
16435
16580
  );
16436
16581
  }
16437
16582
  }
16583
+ var DEBUG_TAIL_WINDOW_MS = 12e4;
16584
+ var DEBUG_TAIL_INTERVAL_MS = 3e3;
16585
+ var DEBUG_TAIL_MAX_CONSECUTIVE_FAILURES = 4;
16586
+ var activeDebugTailExpiresAtMs = null;
16587
+ function debugAuditLog(line) {
16588
+ process.stderr.write(`slack-channel(${AGENT_CODE_NAME ?? "unknown"}): /debug ${line}
16589
+ `);
16590
+ }
16591
+ function debugSleep(ms) {
16592
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
16593
+ }
16594
+ function nowUtcLabel() {
16595
+ return `${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19)} UTC`;
16596
+ }
16597
+ async function updateSlackMessage(channel, ts, text) {
16598
+ try {
16599
+ const res = await fetch("https://slack.com/api/chat.update", {
16600
+ method: "POST",
16601
+ headers: {
16602
+ "Content-Type": "application/json; charset=utf-8",
16603
+ Authorization: `Bearer ${BOT_TOKEN}`
16604
+ },
16605
+ body: JSON.stringify({ channel, ts, text }),
16606
+ signal: AbortSignal.timeout(SLACK_DOWNLOAD_TIMEOUT_MS)
16607
+ });
16608
+ const retryAfterHeader = res.headers.get("retry-after");
16609
+ const retryAfterSec = retryAfterHeader ? parseInt(retryAfterHeader, 10) : NaN;
16610
+ const retryAfterMs = Number.isFinite(retryAfterSec) ? Math.max(0, retryAfterSec * 1e3) : null;
16611
+ const data = await res.json().catch(() => ({ ok: false, error: `http-${res.status}` }));
16612
+ return { ok: data.ok === true, error: data.error, retryAfterMs };
16613
+ } catch (err) {
16614
+ return { ok: false, error: err.message, retryAfterMs: null };
16615
+ }
16616
+ }
16617
+ async function runDebugTailLoop(opts) {
16618
+ const expiresAtMs = opts.expiresAtMs;
16619
+ let last = opts.initialFrame;
16620
+ let consecutiveFailures = 0;
16621
+ let delayMs = DEBUG_TAIL_INTERVAL_MS;
16622
+ try {
16623
+ while (Date.now() + delayMs < expiresAtMs) {
16624
+ await debugSleep(delayMs);
16625
+ delayMs = DEBUG_TAIL_INTERVAL_MS;
16626
+ const snapshot = await capturePaneSnapshot({
16627
+ codeName: opts.codeName,
16628
+ agentDir: SLACK_AGENT_DIR
16629
+ });
16630
+ if (!snapshot) continue;
16631
+ const body = redactPaneText(snapshot.text);
16632
+ if (body === last.body) continue;
16633
+ const frame = { body, source: snapshot.source, capturedAtLabel: nowUtcLabel() };
16634
+ const secondsRemaining = Math.max(0, Math.round((expiresAtMs - Date.now()) / 1e3));
16635
+ const result = await updateSlackMessage(
16636
+ opts.channel,
16637
+ opts.ts,
16638
+ formatPaneSnapshot({
16639
+ codeName: opts.codeName,
16640
+ text: frame.body,
16641
+ source: frame.source,
16642
+ capturedAtLabel: frame.capturedAtLabel,
16643
+ status: { kind: "live", secondsRemaining }
16644
+ })
16645
+ );
16646
+ if (result.ok) {
16647
+ last = frame;
16648
+ consecutiveFailures = 0;
16649
+ continue;
16650
+ }
16651
+ if (result.retryAfterMs !== null || result.error === "ratelimited") {
16652
+ delayMs = Math.max(result.retryAfterMs ?? 0, DEBUG_TAIL_INTERVAL_MS * 2);
16653
+ continue;
16654
+ }
16655
+ consecutiveFailures++;
16656
+ if (consecutiveFailures >= DEBUG_TAIL_MAX_CONSECUTIVE_FAILURES) {
16657
+ debugAuditLog(
16658
+ `tail aborted after ${consecutiveFailures} consecutive update failures (last: ${result.error ?? "unknown"})`
16659
+ );
16660
+ break;
16661
+ }
16662
+ }
16663
+ } finally {
16664
+ activeDebugTailExpiresAtMs = null;
16665
+ await updateSlackMessage(
16666
+ opts.channel,
16667
+ opts.ts,
16668
+ formatPaneSnapshot({
16669
+ codeName: opts.codeName,
16670
+ text: last.body,
16671
+ source: last.source,
16672
+ capturedAtLabel: last.capturedAtLabel,
16673
+ status: { kind: "expired" }
16674
+ })
16675
+ );
16676
+ }
16677
+ }
16678
+ async function handleDebugSlashCommand(payload, responseUrl) {
16679
+ const codeName = AGENT_CODE_NAME ?? "unknown";
16680
+ const verdict = evaluateDebugGate({
16681
+ channelId: payload.channel_id,
16682
+ userId: payload.user_id,
16683
+ allowedUsers: ALLOWED_USERS
16684
+ });
16685
+ if (!verdict.ok) {
16686
+ debugAuditLog(`denied reason=${verdict.reason} user=${payload.user_id ?? "unknown"}`);
16687
+ const denialText = verdict.reason === "not-dm" ? `:warning: \`/debug\` only works in a direct message with \`${codeName}\` \u2014 it shows the agent's raw terminal, so it stays 1:1. Open a DM with the bot and run \`/debug\` there.` : verdict.reason === "allowlist-empty" ? `\u{1F6AB} \`/debug\` is disabled for \`${codeName}\` \u2014 no diagnostic allowlist is configured. Because it exposes the agent's raw terminal, \`/debug\` requires a non-empty \`SLACK_ALLOWED_USERS\` on the host (unlike \`/restart\`, it does not open up when the allowlist is unset).` : `\u{1F6AB} \`/debug\` denied \u2014 your Slack user isn't on the diagnostic allowlist for \`${codeName}\`. Ask whoever operates this host to add you to \`SLACK_ALLOWED_USERS\`.`;
16688
+ await postEphemeralViaResponseUrl(responseUrl, denialText, codeName);
16689
+ return;
16690
+ }
16691
+ const channelId = payload.channel_id;
16692
+ if (!channelId || !BOT_TOKEN) {
16693
+ debugAuditLog(
16694
+ `not-started reason=missing-${channelId ? "bot-token" : "channel"} user=${payload.user_id ?? "unknown"}`
16695
+ );
16696
+ await postEphemeralViaResponseUrl(
16697
+ responseUrl,
16698
+ `:warning: \`/debug\` can't run \u2014 this channel process has no bot token wired.`,
16699
+ codeName
16700
+ );
16701
+ return;
16702
+ }
16703
+ if (activeDebugTailExpiresAtMs !== null && activeDebugTailExpiresAtMs > Date.now()) {
16704
+ const remaining = formatCountdown((activeDebugTailExpiresAtMs - Date.now()) / 1e3);
16705
+ debugAuditLog(
16706
+ `not-started reason=tail-already-running user=${payload.user_id ?? "unknown"} remaining=${remaining}`
16707
+ );
16708
+ await postEphemeralViaResponseUrl(
16709
+ responseUrl,
16710
+ `:hourglass: A \`/debug\` tail is already running for \`${codeName}\` (one at a time) \u2014 it expires in ${remaining}.`,
16711
+ codeName
16712
+ );
16713
+ return;
16714
+ }
16715
+ const reservedExpiresAtMs = Date.now() + DEBUG_TAIL_WINDOW_MS;
16716
+ activeDebugTailExpiresAtMs = reservedExpiresAtMs;
16717
+ const snapshot = await capturePaneSnapshot({ codeName, agentDir: SLACK_AGENT_DIR });
16718
+ if (!snapshot) {
16719
+ activeDebugTailExpiresAtMs = null;
16720
+ debugAuditLog(`not-started reason=no-pane-output user=${payload.user_id ?? "unknown"}`);
16721
+ await postEphemeralViaResponseUrl(
16722
+ responseUrl,
16723
+ `:warning: No pane output available for \`${codeName}\` yet \u2014 the agent session may not have started. (If the whole host is wedged, \`/debug\` can't reach it either \u2014 fall back to the SSM diagnostics runbook.)`,
16724
+ codeName
16725
+ );
16726
+ return;
16727
+ }
16728
+ debugAuditLog(`granted user=${payload.user_id ?? "unknown"} \u2014 starting live tail (source=${snapshot.source})`);
16729
+ const initialFrame = {
16730
+ body: redactPaneText(snapshot.text),
16731
+ source: snapshot.source,
16732
+ capturedAtLabel: nowUtcLabel()
16733
+ };
16734
+ const posted = await postSlackMessageWithTs({
16735
+ channel: channelId,
16736
+ text: formatPaneSnapshot({
16737
+ codeName,
16738
+ text: initialFrame.body,
16739
+ source: initialFrame.source,
16740
+ capturedAtLabel: initialFrame.capturedAtLabel,
16741
+ status: {
16742
+ kind: "live",
16743
+ secondsRemaining: Math.max(0, Math.round((reservedExpiresAtMs - Date.now()) / 1e3))
16744
+ }
16745
+ })
16746
+ });
16747
+ if (!posted.ok || !posted.ts) {
16748
+ activeDebugTailExpiresAtMs = null;
16749
+ debugAuditLog(
16750
+ `not-started reason=post-failed error=${posted.error ?? "unknown"} user=${payload.user_id ?? "unknown"}`
16751
+ );
16752
+ await postEphemeralViaResponseUrl(
16753
+ responseUrl,
16754
+ `:x: Failed to post the pane snapshot${posted.error ? ` (${posted.error})` : ""}. Try again in a moment.`,
16755
+ codeName
16756
+ );
16757
+ return;
16758
+ }
16759
+ void runDebugTailLoop({
16760
+ channel: channelId,
16761
+ ts: posted.ts,
16762
+ codeName,
16763
+ initialFrame,
16764
+ expiresAtMs: reservedExpiresAtMs
16765
+ }).catch((err) => {
16766
+ activeDebugTailExpiresAtMs = null;
16767
+ debugAuditLog(`tail loop crashed: ${redactAugmentedPaths2(err.message)}`);
16768
+ });
16769
+ }
16438
16770
  async function handleSlashCommandEnvelope(payload) {
16439
16771
  const command = payload.command;
16440
16772
  const responseUrl = payload.response_url;
@@ -16497,7 +16829,7 @@ async function handleSlashCommandEnvelope(payload) {
16497
16829
  );
16498
16830
  } catch (err) {
16499
16831
  process.stderr.write(
16500
- `slack-channel(${codeName}): /restart slash-command flag write failed: ${redactAugmentedPaths(err.message)}
16832
+ `slack-channel(${codeName}): /restart slash-command flag write failed: ${redactAugmentedPaths2(err.message)}
16501
16833
  `
16502
16834
  );
16503
16835
  await postEphemeralViaResponseUrl(
@@ -16508,6 +16840,10 @@ async function handleSlashCommandEnvelope(payload) {
16508
16840
  }
16509
16841
  return;
16510
16842
  }
16843
+ if (command === "/debug") {
16844
+ await handleDebugSlashCommand(payload, responseUrl);
16845
+ return;
16846
+ }
16511
16847
  if (command === "/kill" || command === "/unkill") {
16512
16848
  if (!AGT_HOST || !AGT_API_KEY || !AGT_AGENT_ID) {
16513
16849
  await postEphemeralViaResponseUrl(
@@ -16611,7 +16947,7 @@ async function handleRestartCommand(opts) {
16611
16947
  }
16612
16948
  } catch (err) {
16613
16949
  process.stderr.write(
16614
- `slack-channel(${codeName}): /restart flag write failed: ${redactAugmentedPaths(err.message)}
16950
+ `slack-channel(${codeName}): /restart flag write failed: ${redactAugmentedPaths2(err.message)}
16615
16951
  `
16616
16952
  );
16617
16953
  await postSlackMessage({
@@ -17253,14 +17589,14 @@ ${result.formatted}` : "Thread is empty or not found."
17253
17589
  let bytes;
17254
17590
  let size;
17255
17591
  try {
17256
- const stat = statSync2(resolvedPath);
17257
- if (!stat.isFile()) {
17592
+ const stat2 = statSync2(resolvedPath);
17593
+ if (!stat2.isFile()) {
17258
17594
  return {
17259
17595
  content: [{ type: "text", text: `Upload refused: ${resolvedPath} is not a regular file.` }],
17260
17596
  isError: true
17261
17597
  };
17262
17598
  }
17263
- size = stat.size;
17599
+ size = stat2.size;
17264
17600
  bytes = readFileSync4(resolvedPath);
17265
17601
  } catch (err) {
17266
17602
  return {
@@ -17371,7 +17707,7 @@ ${result.formatted}` : "Thread is empty or not found."
17371
17707
  return {
17372
17708
  content: [{
17373
17709
  type: "text",
17374
- text: `Failed to download attachment: ${redactAugmentedPaths(err.message)}`
17710
+ text: `Failed to download attachment: ${redactAugmentedPaths2(err.message)}`
17375
17711
  }],
17376
17712
  isError: true
17377
17713
  };
@@ -17928,7 +18264,7 @@ function isDownloadableFileId(fileId, channel) {
17928
18264
  }
17929
18265
  return entry.channel === channel;
17930
18266
  }
17931
- function redactAugmentedPaths(msg) {
18267
+ function redactAugmentedPaths2(msg) {
17932
18268
  return msg.replaceAll(
17933
18269
  new RegExp(`${homedir2().replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")}/\\.augmented/[^\\s'"\`]*`, "g"),
17934
18270
  "<augmented-path>"
@@ -18010,7 +18346,7 @@ async function buildInboundFileMeta(rawFiles, codeName, channel) {
18010
18346
  });
18011
18347
  } catch (err) {
18012
18348
  process.stderr.write(
18013
- `slack-channel: image auto-download failed for ${classified.id}: ${redactAugmentedPaths(err.message)}
18349
+ `slack-channel: image auto-download failed for ${classified.id}: ${redactAugmentedPaths2(err.message)}
18014
18350
  `
18015
18351
  );
18016
18352
  registerDownloadableFileId(classified.id, channel);
@@ -21,8 +21,8 @@ import {
21
21
  stopPersistentSession,
22
22
  takeZombieDetection,
23
23
  writePersistentClaudeWrapper
24
- } from "./chunk-3JXDBRNG.js";
25
- import "./chunk-GNIA4KN5.js";
24
+ } from "./chunk-S2QLE5BQ.js";
25
+ import "./chunk-TXE2LLKI.js";
26
26
  import "./chunk-XWVM4KPK.js";
27
27
  export {
28
28
  SEND_KEYS_ENTER_DELAY_MS,
@@ -48,4 +48,4 @@ export {
48
48
  takeZombieDetection,
49
49
  writePersistentClaudeWrapper
50
50
  };
51
- //# sourceMappingURL=persistent-session-B5SRS4N4.js.map
51
+ //# sourceMappingURL=persistent-session-BTXWOKJ6.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  paneLogPath
3
- } from "./chunk-3JXDBRNG.js";
4
- import "./chunk-GNIA4KN5.js";
3
+ } from "./chunk-S2QLE5BQ.js";
4
+ import "./chunk-TXE2LLKI.js";
5
5
  import "./chunk-XWVM4KPK.js";
6
6
 
7
7
  // src/lib/responsiveness-probe.ts
@@ -70,4 +70,4 @@ export {
70
70
  collectResponsivenessProbes,
71
71
  getResponsivenessIntervalMs
72
72
  };
73
- //# sourceMappingURL=responsiveness-probe-2QWNZTF4.js.map
73
+ //# sourceMappingURL=responsiveness-probe-Y6TCM6T4.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.27.82",
3
+ "version": "0.27.84",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {