@integrity-labs/agt-cli 0.15.10 → 0.15.11

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.
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __commonJS = (cb, mod) => function __require() {
9
12
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
13
  };
@@ -6798,6 +6801,402 @@ var require_dist = __commonJS({
6798
6801
  }
6799
6802
  });
6800
6803
 
6804
+ // src/slack-block-kit-runtime.ts
6805
+ var slack_block_kit_runtime_exports = {};
6806
+ __export(slack_block_kit_runtime_exports, {
6807
+ buildAskUserBlocks: () => buildAskUserBlocks,
6808
+ buildRatingActionsBlock: () => buildRatingActionsBlock,
6809
+ createPendingInteraction: () => createPendingInteraction,
6810
+ decodeActionId: () => decodeActionId,
6811
+ defaultRatingLabels: () => defaultRatingLabels,
6812
+ encodeActionId: () => encodeActionId,
6813
+ generateOptionToken: () => generateOptionToken,
6814
+ ratingValuesForScale: () => ratingValuesForScale,
6815
+ recordSlackDelivery: () => recordSlackDelivery,
6816
+ resolveInteractive: () => resolveInteractive,
6817
+ updatePendingInteractionMessageTs: () => updatePendingInteractionMessageTs,
6818
+ validateAskUserOptions: () => validateAskUserOptions,
6819
+ validateSlackBlocks: () => validateSlackBlocks,
6820
+ waitForResolution: () => waitForResolution
6821
+ });
6822
+ async function getAuthToken(cfg) {
6823
+ if (auth.token && Date.now() < auth.expiresAtEpochMs) return auth.token;
6824
+ const res = await fetch(`${cfg.apiHost}/host/exchange`, {
6825
+ method: "POST",
6826
+ headers: { "Content-Type": "application/json" },
6827
+ body: JSON.stringify({ host_key: cfg.apiKey }),
6828
+ signal: AbortSignal.timeout(API_FETCH_TIMEOUT_MS)
6829
+ });
6830
+ if (!res.ok) {
6831
+ const body = await res.text().catch(() => "");
6832
+ throw new Error(`AGT auth exchange failed (${res.status}): ${body.slice(0, 200)}`);
6833
+ }
6834
+ const data = await res.json();
6835
+ auth.token = data.token;
6836
+ auth.expiresAtEpochMs = data.expires_at ? new Date(data.expires_at).getTime() - 12e4 : Date.now() + 55 * 6e4;
6837
+ return auth.token;
6838
+ }
6839
+ async function apiCall(cfg, method, path, body) {
6840
+ const token = await getAuthToken(cfg);
6841
+ const buildInit = (authToken) => ({
6842
+ method,
6843
+ headers: {
6844
+ "Content-Type": "application/json",
6845
+ Authorization: `Bearer ${authToken}`
6846
+ },
6847
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {},
6848
+ signal: AbortSignal.timeout(API_FETCH_TIMEOUT_MS)
6849
+ });
6850
+ const res = await fetch(`${cfg.apiHost}${path}`, buildInit(token));
6851
+ if (res.status !== 401) return res;
6852
+ auth.token = null;
6853
+ auth.expiresAtEpochMs = 0;
6854
+ const fresh = await getAuthToken(cfg);
6855
+ return fetch(`${cfg.apiHost}${path}`, buildInit(fresh));
6856
+ }
6857
+ async function createPendingInteraction(cfg, input) {
6858
+ const res = await apiCall(cfg, "POST", "/host/pending-interactions", {
6859
+ agent_id: cfg.agentId,
6860
+ callback_id: input.callbackId,
6861
+ channel_id: input.channelId,
6862
+ message_ts: input.messageTs ?? "",
6863
+ thread_ts: input.threadTs,
6864
+ options: input.options,
6865
+ expires_at: input.expiresAt.toISOString(),
6866
+ kind: input.kind,
6867
+ kind_data: input.kindData
6868
+ });
6869
+ if (!res.ok) {
6870
+ const body = await res.text().catch(() => "");
6871
+ throw new Error(`createPendingInteraction failed (${res.status}): ${body.slice(0, 200)}`);
6872
+ }
6873
+ }
6874
+ async function updatePendingInteractionMessageTs(cfg, callbackId, messageTs) {
6875
+ const res = await apiCall(cfg, "PATCH", `/host/pending-interactions/${encodeURIComponent(callbackId)}`, {
6876
+ agent_id: cfg.agentId,
6877
+ message_ts: messageTs
6878
+ });
6879
+ if (!res.ok) {
6880
+ const body = await res.text().catch(() => "");
6881
+ throw new Error(`updatePendingInteractionMessageTs failed (${res.status}): ${body.slice(0, 200)}`);
6882
+ }
6883
+ }
6884
+ async function waitForResolution(cfg, callbackId, opts) {
6885
+ const interval = opts.pollIntervalMs ?? 500;
6886
+ const deadlineMs = opts.deadline.getTime();
6887
+ let firstPass = true;
6888
+ while (firstPass || Date.now() < deadlineMs) {
6889
+ firstPass = false;
6890
+ try {
6891
+ const res = await apiCall(
6892
+ cfg,
6893
+ "GET",
6894
+ `/host/pending-interactions/${encodeURIComponent(callbackId)}?agent_id=${encodeURIComponent(cfg.agentId)}`
6895
+ );
6896
+ if (res.ok) {
6897
+ const row = await res.json();
6898
+ if (row.status === "resolved") {
6899
+ return {
6900
+ kind: "resolved",
6901
+ value: row.value ?? "",
6902
+ respondedByUser: row.responded_by_user,
6903
+ respondedAt: row.responded_at ?? (/* @__PURE__ */ new Date()).toISOString()
6904
+ };
6905
+ }
6906
+ if (row.status === "timeout") {
6907
+ return { kind: "timeout" };
6908
+ }
6909
+ }
6910
+ } catch {
6911
+ }
6912
+ await sleep(interval);
6913
+ }
6914
+ return { kind: "timeout" };
6915
+ }
6916
+ function sleep(ms) {
6917
+ return new Promise((r) => setTimeout(r, ms));
6918
+ }
6919
+ function generateOptionToken() {
6920
+ const bytes = new Uint8Array(12);
6921
+ const webCrypto = globalThis.crypto;
6922
+ if (webCrypto && typeof webCrypto.getRandomValues === "function") {
6923
+ webCrypto.getRandomValues(bytes);
6924
+ } else {
6925
+ throw new Error(
6926
+ "generateOptionToken: no crypto.getRandomValues implementation available. Need Node 20+ or a Web Crypto polyfill."
6927
+ );
6928
+ }
6929
+ const out = Buffer.from(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
6930
+ return out;
6931
+ }
6932
+ function validateSlackBlocks(blocks) {
6933
+ const errors = [];
6934
+ const cap = 10;
6935
+ const push = (path, message) => {
6936
+ if (errors.length < cap) errors.push({ path, message });
6937
+ };
6938
+ if (!Array.isArray(blocks)) {
6939
+ return { ok: false, errors: [{ path: "blocks", message: "must be an array" }] };
6940
+ }
6941
+ if (blocks.length === 0) {
6942
+ return { ok: false, errors: [{ path: "blocks", message: "must contain at least one block" }] };
6943
+ }
6944
+ if (blocks.length > SLACK_LIMITS.blocksPerMessage) {
6945
+ push("blocks", `max ${SLACK_LIMITS.blocksPerMessage} blocks per message (got ${blocks.length})`);
6946
+ }
6947
+ for (let i = 0; i < blocks.length; i++) {
6948
+ const block = blocks[i];
6949
+ const path = `blocks[${i}]`;
6950
+ const type = block?.type;
6951
+ if (typeof type !== "string") {
6952
+ push(path, "block.type is required");
6953
+ continue;
6954
+ }
6955
+ if (!SUPPORTED_BLOCK_TYPES.includes(type)) {
6956
+ push(path, `block type '${type}' is not supported. Use one of: ${SUPPORTED_BLOCK_TYPES.join(", ")}`);
6957
+ continue;
6958
+ }
6959
+ if (typeof block.block_id === "string" && block.block_id.length > SLACK_LIMITS.identifierChars) {
6960
+ push(`${path}.block_id`, `max ${SLACK_LIMITS.identifierChars} chars`);
6961
+ }
6962
+ switch (type) {
6963
+ case "header":
6964
+ validateHeaderBlock(block, path, push);
6965
+ break;
6966
+ case "section":
6967
+ validateSectionBlock(block, path, push);
6968
+ break;
6969
+ case "context":
6970
+ validateContextBlock(block, path, push);
6971
+ break;
6972
+ case "actions":
6973
+ validateActionsBlock(block, path, push);
6974
+ break;
6975
+ }
6976
+ }
6977
+ return { ok: errors.length === 0, errors };
6978
+ }
6979
+ function validateHeaderBlock(b, path, push) {
6980
+ const text = b.text;
6981
+ if (!text || text.type !== "plain_text" || typeof text.text !== "string") {
6982
+ push(`${path}.text`, "header.text must be { type: 'plain_text', text: string }");
6983
+ return;
6984
+ }
6985
+ if (text.text.length > SLACK_LIMITS.headerTextChars) {
6986
+ push(`${path}.text.text`, `max ${SLACK_LIMITS.headerTextChars} chars`);
6987
+ }
6988
+ }
6989
+ function validateSectionBlock(b, path, push) {
6990
+ const hasText = "text" in b;
6991
+ const hasFields = "fields" in b;
6992
+ if (!hasText && !hasFields) {
6993
+ push(path, "section block requires text or fields");
6994
+ return;
6995
+ }
6996
+ if (hasText) validateTextObject(b.text, `${path}.text`, push, SLACK_LIMITS.sectionTextChars);
6997
+ if (hasFields) {
6998
+ if (!Array.isArray(b.fields)) {
6999
+ push(`${path}.fields`, "must be an array");
7000
+ return;
7001
+ }
7002
+ if (b.fields.length > SLACK_LIMITS.sectionFields) {
7003
+ push(`${path}.fields`, `max ${SLACK_LIMITS.sectionFields} fields (got ${b.fields.length})`);
7004
+ }
7005
+ b.fields.forEach(
7006
+ (f, i) => validateTextObject(f, `${path}.fields[${i}]`, push, SLACK_LIMITS.sectionFieldChars)
7007
+ );
7008
+ }
7009
+ }
7010
+ function validateContextBlock(b, path, push) {
7011
+ if (!Array.isArray(b.elements) || b.elements.length === 0) {
7012
+ push(`${path}.elements`, "context block requires non-empty elements array");
7013
+ return;
7014
+ }
7015
+ b.elements.forEach(
7016
+ (el, i) => validateTextObject(el, `${path}.elements[${i}]`, push, SLACK_LIMITS.sectionFieldChars)
7017
+ );
7018
+ }
7019
+ function validateActionsBlock(b, path, push) {
7020
+ if (!Array.isArray(b.elements) || b.elements.length === 0) {
7021
+ push(`${path}.elements`, "actions block requires non-empty elements array");
7022
+ return;
7023
+ }
7024
+ if (b.elements.length > SLACK_LIMITS.actionElements) {
7025
+ push(`${path}.elements`, `max ${SLACK_LIMITS.actionElements} action elements (got ${b.elements.length})`);
7026
+ }
7027
+ b.elements.forEach((el, i) => {
7028
+ const elPath = `${path}.elements[${i}]`;
7029
+ const e = el;
7030
+ if (typeof e?.type !== "string") {
7031
+ push(elPath, "element.type is required");
7032
+ return;
7033
+ }
7034
+ if (!SUPPORTED_ACTION_ELEMENTS.includes(e.type)) {
7035
+ push(elPath, `element type '${e.type}' is not supported. Use one of: ${SUPPORTED_ACTION_ELEMENTS.join(", ")}`);
7036
+ return;
7037
+ }
7038
+ if (typeof e.action_id !== "string" || e.action_id.length === 0) {
7039
+ push(`${elPath}.action_id`, "required");
7040
+ } else if (e.action_id.length > SLACK_LIMITS.identifierChars) {
7041
+ push(`${elPath}.action_id`, `max ${SLACK_LIMITS.identifierChars} chars`);
7042
+ }
7043
+ if (e.type === "button") {
7044
+ const t = e.text;
7045
+ if (!t || t.type !== "plain_text" || typeof t.text !== "string") {
7046
+ push(`${elPath}.text`, "button.text must be { type: 'plain_text', text: string }");
7047
+ } else if (t.text.length > SLACK_LIMITS.buttonLabelChars) {
7048
+ push(`${elPath}.text.text`, `max ${SLACK_LIMITS.buttonLabelChars} chars`);
7049
+ }
7050
+ }
7051
+ });
7052
+ }
7053
+ function validateTextObject(obj, path, push, maxChars) {
7054
+ if (!obj || typeof obj !== "object") {
7055
+ push(path, "must be a text object");
7056
+ return;
7057
+ }
7058
+ if (obj.type !== "plain_text" && obj.type !== "mrkdwn") {
7059
+ push(`${path}.type`, "must be 'plain_text' or 'mrkdwn'");
7060
+ return;
7061
+ }
7062
+ if (typeof obj.text !== "string") {
7063
+ push(`${path}.text`, "must be a string");
7064
+ return;
7065
+ }
7066
+ if (obj.text.length > maxChars) {
7067
+ push(`${path}.text`, `max ${maxChars} chars (got ${obj.text.length})`);
7068
+ }
7069
+ }
7070
+ function validateAskUserOptions(options) {
7071
+ const errors = [];
7072
+ if (!Array.isArray(options)) {
7073
+ return { ok: false, errors: [{ path: "options", message: "must be an array" }] };
7074
+ }
7075
+ if (options.length === 0) {
7076
+ errors.push({ path: "options", message: "must contain at least one option" });
7077
+ }
7078
+ if (options.length > SLACK_LIMITS.actionElements) {
7079
+ errors.push({ path: "options", message: `max ${SLACK_LIMITS.actionElements} options (one Slack actions block)` });
7080
+ }
7081
+ const seenLabels = /* @__PURE__ */ new Set();
7082
+ const seenValues = /* @__PURE__ */ new Set();
7083
+ options.forEach((opt, i) => {
7084
+ const o = opt;
7085
+ const path = `options[${i}]`;
7086
+ if (typeof o?.label !== "string" || o.label.length === 0) {
7087
+ errors.push({ path: `${path}.label`, message: "required string" });
7088
+ } else {
7089
+ if (o.label.length > SLACK_LIMITS.buttonLabelChars) {
7090
+ errors.push({ path: `${path}.label`, message: `max ${SLACK_LIMITS.buttonLabelChars} chars` });
7091
+ }
7092
+ if (seenLabels.has(o.label)) errors.push({ path: `${path}.label`, message: "duplicate label" });
7093
+ seenLabels.add(o.label);
7094
+ }
7095
+ if (typeof o?.value !== "string" || o.value.length === 0) {
7096
+ errors.push({ path: `${path}.value`, message: "required string" });
7097
+ } else {
7098
+ if (o.value.length > SLACK_LIMITS.optionValueChars) {
7099
+ errors.push({ path: `${path}.value`, message: `max ${SLACK_LIMITS.optionValueChars} chars` });
7100
+ }
7101
+ if (seenValues.has(o.value)) errors.push({ path: `${path}.value`, message: "duplicate value" });
7102
+ seenValues.add(o.value);
7103
+ }
7104
+ });
7105
+ return { ok: errors.length === 0, errors };
7106
+ }
7107
+ function encodeActionId(callbackId, token) {
7108
+ return `aug:${callbackId}:${token}`;
7109
+ }
7110
+ function decodeActionId(actionId) {
7111
+ const m = ACTION_ID_RE.exec(actionId);
7112
+ if (!m) return null;
7113
+ return { callbackId: m[1], token: m[2] };
7114
+ }
7115
+ function buildAskUserBlocks(opts) {
7116
+ return [
7117
+ {
7118
+ type: "section",
7119
+ text: { type: "mrkdwn", text: opts.question }
7120
+ },
7121
+ {
7122
+ type: "actions",
7123
+ elements: opts.options.map(({ label, token }) => ({
7124
+ type: "button",
7125
+ text: { type: "plain_text", text: label },
7126
+ action_id: encodeActionId(opts.callbackId, token)
7127
+ }))
7128
+ }
7129
+ ];
7130
+ }
7131
+ function ratingValuesForScale(scale) {
7132
+ return scale === "1-5" ? [1, 2, 3, 4, 5] : [-1, 0, 1];
7133
+ }
7134
+ function defaultRatingLabels(scale) {
7135
+ return scale === "1-5" ? ["1", "2", "3", "4", "5"] : ["\u{1F44E}", "\u{1F914}", "\u{1F44D}"];
7136
+ }
7137
+ function buildRatingActionsBlock(opts) {
7138
+ return {
7139
+ type: "actions",
7140
+ elements: opts.options.map(({ label, token }) => ({
7141
+ type: "button",
7142
+ text: { type: "plain_text", text: label },
7143
+ action_id: encodeActionId(opts.callbackId, token)
7144
+ }))
7145
+ };
7146
+ }
7147
+ async function resolveInteractive(cfg, input) {
7148
+ const res = await apiCall(cfg, "POST", "/host/interactive/resolve", {
7149
+ agent_id: cfg.agentId,
7150
+ callback_id: input.callbackId,
7151
+ token: input.token,
7152
+ responded_by_user: input.respondedByUser,
7153
+ response_url: input.responseUrl,
7154
+ original_blocks: input.originalBlocks
7155
+ });
7156
+ if (!res.ok) {
7157
+ const body = await res.text().catch(() => "");
7158
+ throw new Error(`resolveInteractive failed (${res.status}): ${body.slice(0, 200)}`);
7159
+ }
7160
+ }
7161
+ async function recordSlackDelivery(cfg, input) {
7162
+ const res = await apiCall(
7163
+ cfg,
7164
+ "POST",
7165
+ `/host/agents/${encodeURIComponent(cfg.agentId)}/slack-delivery`,
7166
+ {
7167
+ channel: input.channel,
7168
+ message_ts: input.messageTs,
7169
+ thread_ts: input.threadTs
7170
+ }
7171
+ );
7172
+ if (!res.ok) {
7173
+ const body = await res.text().catch(() => "");
7174
+ throw new Error(`recordSlackDelivery failed (${res.status}): ${body.slice(0, 200)}`);
7175
+ }
7176
+ }
7177
+ var auth, API_FETCH_TIMEOUT_MS, SLACK_LIMITS, SUPPORTED_BLOCK_TYPES, SUPPORTED_ACTION_ELEMENTS, ACTION_ID_RE;
7178
+ var init_slack_block_kit_runtime = __esm({
7179
+ "src/slack-block-kit-runtime.ts"() {
7180
+ "use strict";
7181
+ auth = { token: null, expiresAtEpochMs: 0 };
7182
+ API_FETCH_TIMEOUT_MS = 1e4;
7183
+ SLACK_LIMITS = {
7184
+ blocksPerMessage: 50,
7185
+ sectionTextChars: 3e3,
7186
+ sectionFields: 10,
7187
+ sectionFieldChars: 2e3,
7188
+ actionElements: 5,
7189
+ buttonLabelChars: 75,
7190
+ identifierChars: 255,
7191
+ optionValueChars: 2e3,
7192
+ headerTextChars: 150
7193
+ };
7194
+ SUPPORTED_BLOCK_TYPES = ["header", "section", "divider", "context", "actions"];
7195
+ SUPPORTED_ACTION_ELEMENTS = ["button"];
7196
+ ACTION_ID_RE = /^aug:([0-9a-f-]{36}):([A-Za-z0-9_-]{1,200})$/;
7197
+ }
7198
+ });
7199
+
6801
7200
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v4/core/core.js
6802
7201
  var NEVER = Object.freeze({
6803
7202
  status: "aborted"
@@ -14164,6 +14563,12 @@ function channelMessageShouldRespond(text, mode) {
14164
14563
  var BOT_TOKEN = process.env.SLACK_BOT_TOKEN;
14165
14564
  var APP_TOKEN = process.env.SLACK_APP_TOKEN;
14166
14565
  var AGENT_CODE_NAME = process.env.AGT_AGENT_CODE_NAME ?? null;
14566
+ var AGT_HOST = process.env.AGT_HOST ?? null;
14567
+ var AGT_API_KEY = process.env.AGT_API_KEY ?? null;
14568
+ var AGT_AGENT_ID = process.env.AGT_AGENT_ID ?? null;
14569
+ var BLOCK_KIT_ENABLED = process.env.SLACK_BLOCK_KIT_ENABLED === "true";
14570
+ var BLOCK_KIT_ASK_USER_ENABLED = process.env.SLACK_BLOCK_KIT_ASK_USER_ENABLED === "true";
14571
+ var BLOCK_KIT_DISABLED = process.env.SLACK_BLOCK_KIT_DISABLED === "true";
14167
14572
  var ALLOWED_USERS = new Set(
14168
14573
  (process.env.SLACK_ALLOWED_USERS ?? "").split(",").map((s) => s.trim()).filter(Boolean)
14169
14574
  );
@@ -14448,6 +14853,9 @@ var RESTART_FLAGS_DIR = join2(homedir2(), ".augmented", "restart-flags");
14448
14853
  function hashChannelId(id) {
14449
14854
  return createHash("sha256").update(id).digest("hex").slice(0, 8);
14450
14855
  }
14856
+ function hashId(id) {
14857
+ return createHash("sha256").update(id).digest("hex").slice(0, 8);
14858
+ }
14451
14859
  async function postSlackMessage(body) {
14452
14860
  try {
14453
14861
  const res = await fetch("https://slack.com/api/chat.postMessage", {
@@ -14710,6 +15118,63 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
14710
15118
  required: ["file_id", "channel"]
14711
15119
  }
14712
15120
  },
15121
+ // ENG-4573: Block Kit tools — only listed when the team has opted in
15122
+ // and the env wiring is present. The kill-switch (SLACK_BLOCK_KIT_DISABLED)
15123
+ // takes precedence so an operator can pull the tools at runtime
15124
+ // without redeploying.
15125
+ ...blockKitToolsAvailable() ? [
15126
+ {
15127
+ name: "slack.send_structured",
15128
+ description: 'Send a Slack message built from Block Kit blocks (structured layout: headers, sections, dividers, context, action buttons). Use when plain text isn\'t enough \u2014 status reports, confirmations with multiple fields, calls-to-action with buttons. Pass a `text` fallback for push notifications. Returns { message_ts, permalink }. Does NOT block waiting for user interactions; for that use slack.ask_user. Pass `interactive: { type: "rate_run", run_id }` to append a 1-5 rating button row that records the score against this scheduled-task run (ENG-4572).',
15129
+ inputSchema: {
15130
+ type: "object",
15131
+ properties: {
15132
+ channel: { type: "string", description: "Slack channel ID" },
15133
+ blocks: {
15134
+ type: "array",
15135
+ description: "Array of Block Kit blocks. Supported block types: header, section, divider, context, actions. Supported interactive elements (inside actions blocks): button. Hard limits: 50 blocks per message, 5 elements per actions block, 3000 chars per section text, 75 chars per button label."
15136
+ },
15137
+ text: { type: "string", description: "Plain-text fallback for push notifications and unfurls. Required." },
15138
+ thread_ts: { type: "string", description: "Thread timestamp for threaded replies (optional)" },
15139
+ interactive: {
15140
+ type: "object",
15141
+ description: `Optional interactive affordance appended to the message. Currently supports type="rate_run" \u2014 appends a 1-5 button row that updates this run's rating when tapped. Use after a scheduled-task delivery so users can rate the firing.`,
15142
+ properties: {
15143
+ type: { type: "string", enum: ["rate_run"], description: 'Affordance kind. Only "rate_run" is supported today.' },
15144
+ run_id: { type: "string", description: "The runs.run_id this rating belongs to (returned by /host/runs/start)." },
15145
+ scale: { type: "string", enum: ["1-5", "sentiment"], description: 'Rating scale. Default "1-5" (1..5 buttons). "sentiment" emits \u{1F44E}/\u{1F914}/\u{1F44D} mapping to -1/0/+1 \u2014 reserved for future Telegram parity.' },
15146
+ expires_in_seconds: { type: "number", description: "How long the rating row stays tappable. Default 86400 (24h). The pending row times out after this; later taps are ignored." }
15147
+ },
15148
+ required: ["type", "run_id"]
15149
+ }
15150
+ },
15151
+ required: ["channel", "blocks", "text"]
15152
+ }
15153
+ }
15154
+ ] : [],
15155
+ ...askUserToolAvailable() ? [
15156
+ {
15157
+ name: "slack.ask_user",
15158
+ description: "Post a question to Slack with multiple-choice buttons and BLOCK until the user taps one (or the timeout fires). Returns the user's choice as { value, label, responded_by, timed_out, responded_at }. Use sparingly \u2014 this stops the agent for up to 5 minutes by default. Prefer plain slack.reply for asking questions where you don't need a structured answer back.",
15159
+ inputSchema: {
15160
+ type: "object",
15161
+ properties: {
15162
+ channel: { type: "string", description: "Slack channel ID" },
15163
+ question: { type: "string", description: "The prompt shown above the buttons. Markdown is supported." },
15164
+ options: {
15165
+ type: "array",
15166
+ description: "Up to 5 options. Each is { label: string (button text, max 75 chars), value: string (returned to you on tap, max 2000 chars) }. Labels and values must be unique within the array."
15167
+ },
15168
+ timeout_seconds: {
15169
+ type: "number",
15170
+ description: "How long to wait for a response. Min 10, max 3600, default 300 (5 min)."
15171
+ },
15172
+ thread_ts: { type: "string", description: "Optional \u2014 post into a specific thread." }
15173
+ },
15174
+ required: ["channel", "question", "options"]
15175
+ }
15176
+ }
15177
+ ] : [],
14713
15178
  {
14714
15179
  name: "slack.upload_file",
14715
15180
  description: "Upload a file from the agent project dir to a Slack channel or thread. Use this for PDFs, images, reports, and any binary deliverables the agent generates. Path must be inside the agent's project directory.",
@@ -14777,6 +15242,30 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
14777
15242
  const trackTs = thread_ts ?? data.ts ?? void 0;
14778
15243
  rememberThread(channel, trackTs, thread_ts ? "mentioned" : "started");
14779
15244
  }
15245
+ if (interactiveHostAvailable() && data.ts) {
15246
+ const cfg = {
15247
+ apiHost: AGT_HOST,
15248
+ apiKey: AGT_API_KEY,
15249
+ agentId: AGT_AGENT_ID
15250
+ };
15251
+ const tsToRecord = data.ts;
15252
+ const threadTsToRecord = thread_ts;
15253
+ void (async () => {
15254
+ try {
15255
+ const runtime = await Promise.resolve().then(() => (init_slack_block_kit_runtime(), slack_block_kit_runtime_exports));
15256
+ await runtime.recordSlackDelivery(cfg, {
15257
+ channel,
15258
+ messageTs: tsToRecord,
15259
+ threadTs: threadTsToRecord
15260
+ });
15261
+ } catch (err) {
15262
+ process.stderr.write(
15263
+ `slack-channel(${AGENT_CODE_NAME}): recordSlackDelivery failed: ${err.message}
15264
+ `
15265
+ );
15266
+ }
15267
+ })();
15268
+ }
14780
15269
  return { content: [{ type: "text", text: "sent" }] };
14781
15270
  } catch (err) {
14782
15271
  return {
@@ -15020,8 +15509,303 @@ ${formatted}` : "Thread is empty or not found."
15020
15509
  };
15021
15510
  }
15022
15511
  }
15512
+ if (name === "slack.send_structured") {
15513
+ return await handleSendStructured(args);
15514
+ }
15515
+ if (name === "slack.ask_user") {
15516
+ return await handleAskUser(args);
15517
+ }
15023
15518
  throw new Error(`Unknown tool: ${name}`);
15024
15519
  });
15520
+ function blockKitToolsAvailable() {
15521
+ return BLOCK_KIT_ENABLED && !BLOCK_KIT_DISABLED;
15522
+ }
15523
+ function askUserToolAvailable() {
15524
+ return blockKitToolsAvailable() && BLOCK_KIT_ASK_USER_ENABLED && Boolean(AGT_HOST && AGT_API_KEY && AGT_AGENT_ID);
15525
+ }
15526
+ function interactiveHostAvailable() {
15527
+ return blockKitToolsAvailable() && Boolean(AGT_HOST && AGT_API_KEY && AGT_AGENT_ID);
15528
+ }
15529
+ async function postSlackMessageWithTs(payload) {
15530
+ try {
15531
+ const res = await fetch("https://slack.com/api/chat.postMessage", {
15532
+ method: "POST",
15533
+ headers: {
15534
+ "Content-Type": "application/json; charset=utf-8",
15535
+ Authorization: `Bearer ${BOT_TOKEN}`
15536
+ },
15537
+ body: JSON.stringify(payload),
15538
+ signal: AbortSignal.timeout(SLACK_DOWNLOAD_TIMEOUT_MS)
15539
+ });
15540
+ return await res.json();
15541
+ } catch (err) {
15542
+ const isTimeout = err.name === "TimeoutError" || err.name === "AbortError";
15543
+ return { ok: false, error: isTimeout ? "timeout" : err.message };
15544
+ }
15545
+ }
15546
+ async function getPermalink(channel, ts) {
15547
+ try {
15548
+ const url = `https://slack.com/api/chat.getPermalink?channel=${encodeURIComponent(channel)}&message_ts=${encodeURIComponent(ts)}`;
15549
+ const res = await fetch(url, {
15550
+ headers: { Authorization: `Bearer ${BOT_TOKEN}` },
15551
+ signal: AbortSignal.timeout(SLACK_DOWNLOAD_TIMEOUT_MS)
15552
+ });
15553
+ const data = await res.json();
15554
+ return data.ok && typeof data.permalink === "string" ? data.permalink : null;
15555
+ } catch {
15556
+ return null;
15557
+ }
15558
+ }
15559
+ async function handleSendStructured(args) {
15560
+ if (!blockKitToolsAvailable()) {
15561
+ return {
15562
+ content: [{ type: "text", text: "slack.send_structured is disabled by team configuration." }],
15563
+ isError: true
15564
+ };
15565
+ }
15566
+ const runtime = await Promise.resolve().then(() => (init_slack_block_kit_runtime(), slack_block_kit_runtime_exports));
15567
+ const { validateSlackBlocks: validateSlackBlocks2 } = runtime;
15568
+ const { channel, blocks, text, thread_ts, interactive } = args;
15569
+ if (typeof channel !== "string" || !channel) {
15570
+ return errResult("channel is required");
15571
+ }
15572
+ if (typeof text !== "string" || !text) {
15573
+ return errResult("text is required (used as fallback for push notifications and unfurls)");
15574
+ }
15575
+ const validation = validateSlackBlocks2(blocks);
15576
+ if (!validation.ok) {
15577
+ return errResult(`Invalid blocks: ${validation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
15578
+ }
15579
+ let effectiveBlocks = blocks;
15580
+ let ratingCallbackId = null;
15581
+ let ratingExpiresAt = null;
15582
+ let ratingTokenised = [];
15583
+ if (interactive) {
15584
+ if (interactive.type !== "rate_run") {
15585
+ return errResult(`Unsupported interactive.type '${interactive.type}'. Supported: rate_run.`);
15586
+ }
15587
+ if (typeof interactive.run_id !== "string" || !interactive.run_id) {
15588
+ return errResult("interactive.run_id is required when interactive is set");
15589
+ }
15590
+ if (!interactiveHostAvailable()) {
15591
+ return errResult(
15592
+ "interactive=rate_run requires Block Kit plus host MCP wiring (AGT_HOST, AGT_API_KEY, AGT_AGENT_ID)."
15593
+ );
15594
+ }
15595
+ const scale = interactive.scale === "sentiment" ? "sentiment" : "1-5";
15596
+ const requestedTtl = typeof interactive.expires_in_seconds === "number" ? interactive.expires_in_seconds : 86400;
15597
+ const ttl = Math.max(60, Math.min(7 * 86400, requestedTtl));
15598
+ const { randomUUID: rndUUID } = await import("crypto");
15599
+ ratingCallbackId = rndUUID();
15600
+ ratingExpiresAt = new Date(Date.now() + ttl * 1e3);
15601
+ const values = runtime.ratingValuesForScale(scale);
15602
+ const labels = runtime.defaultRatingLabels(scale);
15603
+ ratingTokenised = values.map((v, i) => ({
15604
+ token: runtime.generateOptionToken(),
15605
+ value: String(v),
15606
+ label: labels[i] ?? String(v)
15607
+ }));
15608
+ const ratingBlock = runtime.buildRatingActionsBlock({
15609
+ callbackId: ratingCallbackId,
15610
+ options: ratingTokenised.map(({ label, token }) => ({
15611
+ // The numeric value is carried by token → options mapping in the
15612
+ // pending_interaction row, not by the button itself, so this
15613
+ // cast-back to RatingOption only needs label + token + a placeholder.
15614
+ value: 0,
15615
+ label,
15616
+ token
15617
+ }))
15618
+ });
15619
+ effectiveBlocks = [...blocks, ratingBlock];
15620
+ const finalValidation = runtime.validateSlackBlocks(effectiveBlocks);
15621
+ if (!finalValidation.ok) {
15622
+ return errResult(
15623
+ `Invalid blocks (after appending rating row): ${finalValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`
15624
+ );
15625
+ }
15626
+ const cfg = {
15627
+ apiHost: AGT_HOST,
15628
+ apiKey: AGT_API_KEY,
15629
+ agentId: AGT_AGENT_ID
15630
+ };
15631
+ try {
15632
+ await runtime.createPendingInteraction(cfg, {
15633
+ callbackId: ratingCallbackId,
15634
+ channelId: channel,
15635
+ messageTs: "",
15636
+ threadTs: thread_ts,
15637
+ options: ratingTokenised,
15638
+ expiresAt: ratingExpiresAt,
15639
+ kind: "rate_run",
15640
+ kindData: { run_id: interactive.run_id, scale }
15641
+ });
15642
+ } catch (err) {
15643
+ return errResult(`Failed to register rating row: ${err.message}`);
15644
+ }
15645
+ }
15646
+ const result = await postSlackMessageWithTs({
15647
+ channel,
15648
+ blocks: effectiveBlocks,
15649
+ text,
15650
+ ...thread_ts ? { thread_ts } : {}
15651
+ });
15652
+ if (!result.ok || !result.ts) {
15653
+ return errResult(`Slack chat.postMessage failed: ${result.error ?? "unknown"}`);
15654
+ }
15655
+ if (ratingCallbackId) {
15656
+ try {
15657
+ await runtime.updatePendingInteractionMessageTs(
15658
+ { apiHost: AGT_HOST, apiKey: AGT_API_KEY, agentId: AGT_AGENT_ID },
15659
+ ratingCallbackId,
15660
+ result.ts
15661
+ );
15662
+ } catch (err) {
15663
+ process.stderr.write(
15664
+ `slack-channel(${AGENT_CODE_NAME}): updatePendingInteractionMessageTs failed for rating ${hashId(ratingCallbackId)}: ${err.message}
15665
+ `
15666
+ );
15667
+ }
15668
+ }
15669
+ if (interactiveHostAvailable() && !ratingCallbackId) {
15670
+ const cfg = {
15671
+ apiHost: AGT_HOST,
15672
+ apiKey: AGT_API_KEY,
15673
+ agentId: AGT_AGENT_ID
15674
+ };
15675
+ const tsToRecord = result.ts;
15676
+ const threadTsToRecord = thread_ts;
15677
+ void (async () => {
15678
+ try {
15679
+ await runtime.recordSlackDelivery(cfg, {
15680
+ channel,
15681
+ messageTs: tsToRecord,
15682
+ threadTs: threadTsToRecord
15683
+ });
15684
+ } catch (err) {
15685
+ process.stderr.write(
15686
+ `slack-channel(${AGENT_CODE_NAME}): recordSlackDelivery (send_structured) failed: ${err.message}
15687
+ `
15688
+ );
15689
+ }
15690
+ })();
15691
+ }
15692
+ const permalink = await getPermalink(channel, result.ts);
15693
+ return {
15694
+ content: [
15695
+ {
15696
+ type: "text",
15697
+ text: JSON.stringify({
15698
+ message_ts: result.ts,
15699
+ permalink: permalink ?? "",
15700
+ ...ratingCallbackId ? { rating_callback_id: ratingCallbackId } : {}
15701
+ })
15702
+ }
15703
+ ]
15704
+ };
15705
+ }
15706
+ async function handleAskUser(args) {
15707
+ if (!askUserToolAvailable()) {
15708
+ return errResult("slack.ask_user is disabled or the host MCP is missing AGT_HOST/AGT_API_KEY/AGT_AGENT_ID env wiring.");
15709
+ }
15710
+ const { randomUUID: rndUUID } = await import("crypto");
15711
+ const runtime = await Promise.resolve().then(() => (init_slack_block_kit_runtime(), slack_block_kit_runtime_exports));
15712
+ const { validateAskUserOptions: validateAskUserOptions2, buildAskUserBlocks: buildAskUserBlocks2 } = runtime;
15713
+ const { channel, question, options, timeout_seconds, thread_ts } = args;
15714
+ if (typeof channel !== "string" || !channel) return errResult("channel is required");
15715
+ if (typeof question !== "string" || !question) return errResult("question is required");
15716
+ const optsValidation = validateAskUserOptions2(options);
15717
+ if (!optsValidation.ok) {
15718
+ return errResult(`Invalid options: ${optsValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
15719
+ }
15720
+ const requestedTimeout = typeof timeout_seconds === "number" ? timeout_seconds : 300;
15721
+ const timeoutSec = Math.max(10, Math.min(3600, requestedTimeout));
15722
+ const callbackId = rndUUID();
15723
+ const optionsArr = options;
15724
+ const tokenised = optionsArr.map(({ label, value }) => ({
15725
+ token: runtime.generateOptionToken(),
15726
+ label,
15727
+ value
15728
+ }));
15729
+ const blocks = buildAskUserBlocks2({
15730
+ callbackId,
15731
+ question,
15732
+ options: tokenised.map(({ label, token }) => ({ label, token }))
15733
+ });
15734
+ const cfg = {
15735
+ apiHost: AGT_HOST,
15736
+ apiKey: AGT_API_KEY,
15737
+ agentId: AGT_AGENT_ID
15738
+ };
15739
+ const expiresAt = new Date(Date.now() + timeoutSec * 1e3);
15740
+ try {
15741
+ await runtime.createPendingInteraction(cfg, {
15742
+ callbackId,
15743
+ channelId: channel,
15744
+ messageTs: "",
15745
+ // placeholder — patched after Slack returns the real ts
15746
+ threadTs: thread_ts,
15747
+ options: tokenised,
15748
+ expiresAt
15749
+ });
15750
+ } catch (err) {
15751
+ return errResult(`Failed to register pending interaction: ${err.message}`);
15752
+ }
15753
+ const slackResult = await postSlackMessageWithTs({
15754
+ channel,
15755
+ blocks,
15756
+ text: question,
15757
+ // fallback for push notifications
15758
+ ...thread_ts ? { thread_ts } : {}
15759
+ });
15760
+ if (!slackResult.ok || !slackResult.ts) {
15761
+ return errResult(`Slack chat.postMessage failed: ${slackResult.error ?? "unknown"}`);
15762
+ }
15763
+ try {
15764
+ await runtime.updatePendingInteractionMessageTs(cfg, callbackId, slackResult.ts);
15765
+ } catch (err) {
15766
+ process.stderr.write(
15767
+ `slack-channel(${AGENT_CODE_NAME}): updatePendingInteractionMessageTs failed for ${callbackId}: ${err.message}
15768
+ `
15769
+ );
15770
+ }
15771
+ const resolution = await runtime.waitForResolution(cfg, callbackId, {
15772
+ deadline: expiresAt
15773
+ });
15774
+ if (resolution.kind === "timeout") {
15775
+ return {
15776
+ content: [
15777
+ {
15778
+ type: "text",
15779
+ text: JSON.stringify({
15780
+ value: null,
15781
+ label: null,
15782
+ responded_by: null,
15783
+ responded_at: null,
15784
+ timed_out: true
15785
+ })
15786
+ }
15787
+ ]
15788
+ };
15789
+ }
15790
+ const matched = tokenised.find((o) => o.value === resolution.value);
15791
+ return {
15792
+ content: [
15793
+ {
15794
+ type: "text",
15795
+ text: JSON.stringify({
15796
+ value: resolution.value,
15797
+ label: matched?.label ?? resolution.value,
15798
+ responded_by: resolution.respondedByUser,
15799
+ responded_at: resolution.respondedAt,
15800
+ timed_out: false
15801
+ })
15802
+ }
15803
+ ]
15804
+ };
15805
+ }
15806
+ function errResult(text) {
15807
+ return { content: [{ type: "text", text }], isError: true };
15808
+ }
15025
15809
  var MAX_INBOUND_FILE_BYTES = 25 * 1024 * 1024;
15026
15810
  var SLACK_DOWNLOAD_TIMEOUT_MS = 15e3;
15027
15811
  var DOWNLOAD_ALLOWLIST_TTL_MS = 60 * 60 * 1e3;
@@ -15217,6 +16001,44 @@ async function connectSocketMode() {
15217
16001
  if (msg.envelope_id) {
15218
16002
  ws.send(JSON.stringify({ envelope_id: msg.envelope_id }));
15219
16003
  }
16004
+ if (msg.type === "interactive" && msg.payload?.type === "block_actions") {
16005
+ if (!interactiveHostAvailable()) {
16006
+ process.stderr.write(
16007
+ `slack-channel(${AGENT_CODE_NAME}): block_actions received but Block Kit + host MCP wiring missing \u2014 dropping
16008
+ `
16009
+ );
16010
+ return;
16011
+ }
16012
+ const action = msg.payload.actions?.[0];
16013
+ if (!action || typeof action.action_id !== "string") return;
16014
+ const runtime = await Promise.resolve().then(() => (init_slack_block_kit_runtime(), slack_block_kit_runtime_exports));
16015
+ const decoded = runtime.decodeActionId(action.action_id);
16016
+ if (!decoded) {
16017
+ return;
16018
+ }
16019
+ try {
16020
+ await runtime.resolveInteractive(
16021
+ {
16022
+ apiHost: AGT_HOST,
16023
+ apiKey: AGT_API_KEY,
16024
+ agentId: AGT_AGENT_ID
16025
+ },
16026
+ {
16027
+ callbackId: decoded.callbackId,
16028
+ token: decoded.token,
16029
+ respondedByUser: msg.payload.user?.id,
16030
+ originalBlocks: msg.payload.message?.blocks,
16031
+ responseUrl: msg.payload.response_url
16032
+ }
16033
+ );
16034
+ } catch (err) {
16035
+ process.stderr.write(
16036
+ `slack-channel(${AGENT_CODE_NAME}): resolveInteractive failed for ${hashId(decoded.callbackId)}: ${err.message}
16037
+ `
16038
+ );
16039
+ }
16040
+ return;
16041
+ }
15220
16042
  if (msg.type !== "events_api" || !msg.payload?.event) return;
15221
16043
  const evt = msg.payload.event;
15222
16044
  if (evt.user === botUserId) return;