@integrity-labs/agt-cli 0.15.7 → 0.15.9
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/dist/bin/agt.js +3 -3
- package/dist/{chunk-WIW5FIRY.js → chunk-C6UBNLUC.js} +124 -5
- package/dist/chunk-C6UBNLUC.js.map +1 -0
- package/dist/lib/manager-worker.js +348 -177
- package/dist/lib/manager-worker.js.map +1 -1
- package/mcp/index.js +9 -0
- package/mcp/slack-channel.js +397 -16
- package/mcp/telegram-channel.js +421 -12
- package/package.json +1 -1
- package/dist/chunk-WIW5FIRY.js.map +0 -1
package/mcp/index.js
CHANGED
|
@@ -21040,6 +21040,12 @@ var AGT_API_KEY = process.env.AGT_API_KEY;
|
|
|
21040
21040
|
var AGT_AGENT_ID = process.env.AGT_AGENT_ID;
|
|
21041
21041
|
var AGT_AGENT_CODE_NAME = process.env.AGT_AGENT_CODE_NAME;
|
|
21042
21042
|
var AGT_APP_URL = (process.env.AGT_APP_URL ?? "").replace(/\/+$/, "");
|
|
21043
|
+
function readRunId() {
|
|
21044
|
+
const raw = process.env.AGT_RUN_ID ?? "";
|
|
21045
|
+
if (!raw || raw === "${AGT_RUN_ID}") return null;
|
|
21046
|
+
return raw;
|
|
21047
|
+
}
|
|
21048
|
+
var AGT_RUN_ID = readRunId();
|
|
21043
21049
|
var AGT_TOKEN = process.env.AGT_TOKEN ?? "";
|
|
21044
21050
|
if (!AGT_HOST || !AGT_AGENT_ID || !AGT_TOKEN && !AGT_API_KEY) {
|
|
21045
21051
|
console.error(
|
|
@@ -21166,6 +21172,7 @@ server.tool(
|
|
|
21166
21172
|
async (params) => {
|
|
21167
21173
|
const data = await apiPost("/host/kanban", {
|
|
21168
21174
|
agent_id: AGT_AGENT_ID,
|
|
21175
|
+
run_id: AGT_RUN_ID,
|
|
21169
21176
|
add: [
|
|
21170
21177
|
{
|
|
21171
21178
|
title: params.title,
|
|
@@ -21791,6 +21798,8 @@ server.tool(
|
|
|
21791
21798
|
async (params) => {
|
|
21792
21799
|
const data = await apiPost("/host/knowledge", {
|
|
21793
21800
|
agent_id: AGT_AGENT_ID,
|
|
21801
|
+
// ENG-4538 will land a run_id column on team_knowledge; until then
|
|
21802
|
+
// the /host/knowledge endpoint has nowhere to stamp AGT_RUN_ID.
|
|
21794
21803
|
add: [
|
|
21795
21804
|
{
|
|
21796
21805
|
title: params.title,
|
package/mcp/slack-channel.js
CHANGED
|
@@ -13887,9 +13887,22 @@ var StdioServerTransport = class {
|
|
|
13887
13887
|
};
|
|
13888
13888
|
|
|
13889
13889
|
// src/slack-channel.ts
|
|
13890
|
-
import {
|
|
13890
|
+
import {
|
|
13891
|
+
chmodSync,
|
|
13892
|
+
createWriteStream,
|
|
13893
|
+
existsSync,
|
|
13894
|
+
mkdirSync as mkdirSync2,
|
|
13895
|
+
readFileSync as readFileSync2,
|
|
13896
|
+
readdirSync,
|
|
13897
|
+
renameSync,
|
|
13898
|
+
statSync,
|
|
13899
|
+
unlinkSync,
|
|
13900
|
+
watch,
|
|
13901
|
+
writeFileSync as writeFileSync2
|
|
13902
|
+
} from "fs";
|
|
13891
13903
|
import { basename, join as join2, resolve as resolve2 } from "path";
|
|
13892
13904
|
import { homedir as homedir2 } from "os";
|
|
13905
|
+
import { createHash, randomUUID } from "crypto";
|
|
13893
13906
|
|
|
13894
13907
|
// src/slack-thread-store.ts
|
|
13895
13908
|
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -14156,17 +14169,254 @@ var ALLOWED_USERS = new Set(
|
|
|
14156
14169
|
);
|
|
14157
14170
|
var THREAD_AUTO_FOLLOW = process.env.SLACK_THREAD_AUTO_FOLLOW ?? "off";
|
|
14158
14171
|
var CHANNEL_RESPONSE_MODE = parseResponseMode(process.env.SLACK_CHANNEL_RESPONSE_MODE);
|
|
14159
|
-
var RESPONSE_TIMEOUT_MS =
|
|
14172
|
+
var RESPONSE_TIMEOUT_MS = 3e5;
|
|
14160
14173
|
var pendingMessages = /* @__PURE__ */ new Map();
|
|
14174
|
+
var SLACK_AGENT_DIR = AGENT_CODE_NAME ? join2(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
|
|
14175
|
+
var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ? join2(SLACK_AGENT_DIR, "slack-pending-inbound") : null;
|
|
14176
|
+
var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ? join2(SLACK_AGENT_DIR, "slack-recovery-outbox") : null;
|
|
14177
|
+
var SLACK_MAX_RECOVERY_ATTEMPTS = 3;
|
|
14178
|
+
function redactSlackId(id) {
|
|
14179
|
+
if (!id) return "<none>";
|
|
14180
|
+
return createHash("sha256").update(id).digest("hex").slice(0, 8);
|
|
14181
|
+
}
|
|
14182
|
+
function safeSlackMarkerName(channel, threadTs, messageTs) {
|
|
14183
|
+
const safe = (s) => s.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
14184
|
+
return `${safe(channel)}__${safe(threadTs)}__${safe(messageTs)}.json`;
|
|
14185
|
+
}
|
|
14186
|
+
function slackPendingInboundPath(channel, threadTs, messageTs) {
|
|
14187
|
+
if (!SLACK_PENDING_INBOUND_DIR) return null;
|
|
14188
|
+
return join2(SLACK_PENDING_INBOUND_DIR, safeSlackMarkerName(channel, threadTs, messageTs));
|
|
14189
|
+
}
|
|
14190
|
+
function writeSlackPendingInboundMarker(channel, threadTs, messageTs) {
|
|
14191
|
+
const path = slackPendingInboundPath(channel, threadTs, messageTs);
|
|
14192
|
+
if (!path || !SLACK_PENDING_INBOUND_DIR) return;
|
|
14193
|
+
const marker = {
|
|
14194
|
+
channel,
|
|
14195
|
+
thread_ts: threadTs,
|
|
14196
|
+
message_ts: messageTs,
|
|
14197
|
+
received_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
14198
|
+
};
|
|
14199
|
+
try {
|
|
14200
|
+
mkdirSync2(SLACK_PENDING_INBOUND_DIR, { recursive: true, mode: 448 });
|
|
14201
|
+
writeFileSync2(path, JSON.stringify(marker), { mode: 384 });
|
|
14202
|
+
} catch (err) {
|
|
14203
|
+
process.stderr.write(
|
|
14204
|
+
`slack-channel(${AGENT_CODE_NAME}): pending-inbound marker write failed: ${err.message}
|
|
14205
|
+
`
|
|
14206
|
+
);
|
|
14207
|
+
}
|
|
14208
|
+
}
|
|
14209
|
+
function clearSlackPendingInboundMarker(channel, threadTs, messageTs) {
|
|
14210
|
+
const path = slackPendingInboundPath(channel, threadTs, messageTs);
|
|
14211
|
+
if (!path) return;
|
|
14212
|
+
try {
|
|
14213
|
+
if (existsSync(path)) unlinkSync(path);
|
|
14214
|
+
} catch {
|
|
14215
|
+
}
|
|
14216
|
+
}
|
|
14217
|
+
function clearAllSlackPendingMarkersForThread(channel, threadTs) {
|
|
14218
|
+
if (!SLACK_PENDING_INBOUND_DIR) return;
|
|
14219
|
+
const safeChan = channel.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
14220
|
+
const safeThread = threadTs.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
14221
|
+
const prefix = `${safeChan}__${safeThread}__`;
|
|
14222
|
+
try {
|
|
14223
|
+
for (const f of readdirSync(SLACK_PENDING_INBOUND_DIR)) {
|
|
14224
|
+
if (!f.startsWith(prefix) || !f.endsWith(".json")) continue;
|
|
14225
|
+
try {
|
|
14226
|
+
unlinkSync(join2(SLACK_PENDING_INBOUND_DIR, f));
|
|
14227
|
+
} catch {
|
|
14228
|
+
}
|
|
14229
|
+
}
|
|
14230
|
+
} catch {
|
|
14231
|
+
}
|
|
14232
|
+
}
|
|
14233
|
+
function slackNextRetryName(filename) {
|
|
14234
|
+
const match = filename.match(/^(.*?)(?:\.retry-(\d+))?\.json$/);
|
|
14235
|
+
if (!match) return null;
|
|
14236
|
+
const base = match[1];
|
|
14237
|
+
const prev = match[2] ? parseInt(match[2], 10) : 0;
|
|
14238
|
+
const attempt = prev + 1;
|
|
14239
|
+
if (attempt >= SLACK_MAX_RECOVERY_ATTEMPTS) {
|
|
14240
|
+
return { next: `${base}.retry-${attempt}.poison.json`, attempt };
|
|
14241
|
+
}
|
|
14242
|
+
return { next: `${base}.retry-${attempt}.json`, attempt };
|
|
14243
|
+
}
|
|
14244
|
+
async function processSlackRecoveryOutboxFile(filename) {
|
|
14245
|
+
if (!SLACK_RECOVERY_OUTBOX_DIR) return;
|
|
14246
|
+
if (filename.endsWith(".poison.json") || filename.endsWith(".tmp")) return;
|
|
14247
|
+
const fullPath = join2(SLACK_RECOVERY_OUTBOX_DIR, filename);
|
|
14248
|
+
let payload;
|
|
14249
|
+
try {
|
|
14250
|
+
payload = JSON.parse(readFileSync2(fullPath, "utf-8"));
|
|
14251
|
+
} catch (err) {
|
|
14252
|
+
process.stderr.write(
|
|
14253
|
+
`slack-channel(${AGENT_CODE_NAME}): recovery outbox parse failed (${filename}): ${err.message}
|
|
14254
|
+
`
|
|
14255
|
+
);
|
|
14256
|
+
try {
|
|
14257
|
+
renameSync(fullPath, `${fullPath}.parse-error.poison`);
|
|
14258
|
+
} catch {
|
|
14259
|
+
}
|
|
14260
|
+
return;
|
|
14261
|
+
}
|
|
14262
|
+
if (!payload.channel || !payload.text) {
|
|
14263
|
+
process.stderr.write(
|
|
14264
|
+
`slack-channel(${AGENT_CODE_NAME}): recovery outbox malformed (${filename}): missing channel or text
|
|
14265
|
+
`
|
|
14266
|
+
);
|
|
14267
|
+
try {
|
|
14268
|
+
renameSync(fullPath, `${fullPath}.malformed.poison`);
|
|
14269
|
+
} catch {
|
|
14270
|
+
}
|
|
14271
|
+
return;
|
|
14272
|
+
}
|
|
14273
|
+
const text = `[recovered reply] ${payload.text}`;
|
|
14274
|
+
let sendSucceeded = false;
|
|
14275
|
+
const controller = new AbortController();
|
|
14276
|
+
const timeoutId = setTimeout(() => controller.abort(), 15e3);
|
|
14277
|
+
try {
|
|
14278
|
+
const res = await fetch("https://slack.com/api/chat.postMessage", {
|
|
14279
|
+
method: "POST",
|
|
14280
|
+
signal: controller.signal,
|
|
14281
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${BOT_TOKEN}` },
|
|
14282
|
+
body: JSON.stringify({
|
|
14283
|
+
channel: payload.channel,
|
|
14284
|
+
text,
|
|
14285
|
+
...payload.thread_ts ? { thread_ts: payload.thread_ts } : {}
|
|
14286
|
+
})
|
|
14287
|
+
});
|
|
14288
|
+
const data = await res.json();
|
|
14289
|
+
if (data.ok) {
|
|
14290
|
+
sendSucceeded = true;
|
|
14291
|
+
process.stderr.write(
|
|
14292
|
+
`slack-channel(${AGENT_CODE_NAME}): ghost-reply recovery sent (channel=${redactSlackId(payload.channel)} thread=${redactSlackId(payload.thread_ts)})
|
|
14293
|
+
`
|
|
14294
|
+
);
|
|
14295
|
+
if (payload.thread_ts) {
|
|
14296
|
+
clearAllSlackPendingMarkersForThread(payload.channel, payload.thread_ts);
|
|
14297
|
+
}
|
|
14298
|
+
} else {
|
|
14299
|
+
process.stderr.write(
|
|
14300
|
+
`slack-channel(${AGENT_CODE_NAME}): ghost-reply recovery failed (channel=${redactSlackId(payload.channel)}): ${data.error ?? "unknown"}
|
|
14301
|
+
`
|
|
14302
|
+
);
|
|
14303
|
+
}
|
|
14304
|
+
} catch (err) {
|
|
14305
|
+
const msg = err.name === "AbortError" ? "timed out after 15s" : err.message;
|
|
14306
|
+
process.stderr.write(
|
|
14307
|
+
`slack-channel(${AGENT_CODE_NAME}): ghost-reply recovery error (channel=${redactSlackId(payload.channel)}): ${msg}
|
|
14308
|
+
`
|
|
14309
|
+
);
|
|
14310
|
+
} finally {
|
|
14311
|
+
clearTimeout(timeoutId);
|
|
14312
|
+
}
|
|
14313
|
+
if (sendSucceeded) {
|
|
14314
|
+
try {
|
|
14315
|
+
unlinkSync(fullPath);
|
|
14316
|
+
} catch {
|
|
14317
|
+
}
|
|
14318
|
+
return;
|
|
14319
|
+
}
|
|
14320
|
+
const next = slackNextRetryName(filename);
|
|
14321
|
+
if (next) {
|
|
14322
|
+
try {
|
|
14323
|
+
renameSync(fullPath, join2(SLACK_RECOVERY_OUTBOX_DIR, next.next));
|
|
14324
|
+
if (next.attempt >= SLACK_MAX_RECOVERY_ATTEMPTS) {
|
|
14325
|
+
process.stderr.write(
|
|
14326
|
+
`slack-channel(${AGENT_CODE_NAME}): ghost-reply recovery exhausted retries \u2014 moved to ${next.next}
|
|
14327
|
+
`
|
|
14328
|
+
);
|
|
14329
|
+
}
|
|
14330
|
+
} catch {
|
|
14331
|
+
}
|
|
14332
|
+
}
|
|
14333
|
+
}
|
|
14334
|
+
function isFirstAttemptSlackOutboxFile(filename) {
|
|
14335
|
+
if (!filename.endsWith(".json")) return false;
|
|
14336
|
+
if (filename.includes(".retry-")) return false;
|
|
14337
|
+
if (filename.endsWith(".poison.json") || filename.endsWith(".tmp")) return false;
|
|
14338
|
+
if (filename.startsWith(".")) return false;
|
|
14339
|
+
return true;
|
|
14340
|
+
}
|
|
14341
|
+
var SLACK_RETRY_SCAN_INTERVAL_MS = 6e4;
|
|
14342
|
+
var SLACK_RETRY_BACKOFF_BASE_MS = 6e4;
|
|
14343
|
+
function shouldRetrySlackNow(filename, mtimeMs) {
|
|
14344
|
+
const match = filename.match(/\.retry-(\d+)\.json$/);
|
|
14345
|
+
if (!match) return false;
|
|
14346
|
+
const attempt = parseInt(match[1], 10);
|
|
14347
|
+
return Date.now() - mtimeMs >= attempt * SLACK_RETRY_BACKOFF_BASE_MS;
|
|
14348
|
+
}
|
|
14349
|
+
function scanSlackRecoveryRetries() {
|
|
14350
|
+
if (!SLACK_RECOVERY_OUTBOX_DIR) return;
|
|
14351
|
+
let entries;
|
|
14352
|
+
try {
|
|
14353
|
+
entries = readdirSync(SLACK_RECOVERY_OUTBOX_DIR);
|
|
14354
|
+
} catch {
|
|
14355
|
+
return;
|
|
14356
|
+
}
|
|
14357
|
+
for (const f of entries) {
|
|
14358
|
+
if (!f.endsWith(".json")) continue;
|
|
14359
|
+
if (!f.includes(".retry-") || f.endsWith(".poison.json")) continue;
|
|
14360
|
+
let mtimeMs;
|
|
14361
|
+
try {
|
|
14362
|
+
mtimeMs = statSync(join2(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
|
|
14363
|
+
} catch {
|
|
14364
|
+
continue;
|
|
14365
|
+
}
|
|
14366
|
+
if (shouldRetrySlackNow(f, mtimeMs)) {
|
|
14367
|
+
void processSlackRecoveryOutboxFile(f);
|
|
14368
|
+
}
|
|
14369
|
+
}
|
|
14370
|
+
}
|
|
14371
|
+
function startSlackRecoveryOutboxWatcher() {
|
|
14372
|
+
if (!SLACK_RECOVERY_OUTBOX_DIR) return;
|
|
14373
|
+
try {
|
|
14374
|
+
mkdirSync2(SLACK_RECOVERY_OUTBOX_DIR, { recursive: true, mode: 448 });
|
|
14375
|
+
} catch (err) {
|
|
14376
|
+
process.stderr.write(
|
|
14377
|
+
`slack-channel(${AGENT_CODE_NAME}): recovery outbox mkdir failed: ${err.message}
|
|
14378
|
+
`
|
|
14379
|
+
);
|
|
14380
|
+
return;
|
|
14381
|
+
}
|
|
14382
|
+
try {
|
|
14383
|
+
for (const f of readdirSync(SLACK_RECOVERY_OUTBOX_DIR)) {
|
|
14384
|
+
if (isFirstAttemptSlackOutboxFile(f)) void processSlackRecoveryOutboxFile(f);
|
|
14385
|
+
}
|
|
14386
|
+
} catch {
|
|
14387
|
+
}
|
|
14388
|
+
try {
|
|
14389
|
+
const watcher = watch(SLACK_RECOVERY_OUTBOX_DIR, (event, filename) => {
|
|
14390
|
+
if (event !== "rename" || !filename) return;
|
|
14391
|
+
if (!isFirstAttemptSlackOutboxFile(filename)) return;
|
|
14392
|
+
if (existsSync(join2(SLACK_RECOVERY_OUTBOX_DIR, filename))) {
|
|
14393
|
+
void processSlackRecoveryOutboxFile(filename);
|
|
14394
|
+
}
|
|
14395
|
+
});
|
|
14396
|
+
watcher.unref?.();
|
|
14397
|
+
} catch (err) {
|
|
14398
|
+
process.stderr.write(
|
|
14399
|
+
`slack-channel(${AGENT_CODE_NAME}): recovery outbox watch failed: ${err.message}
|
|
14400
|
+
`
|
|
14401
|
+
);
|
|
14402
|
+
}
|
|
14403
|
+
const retryTimer = setInterval(scanSlackRecoveryRetries, SLACK_RETRY_SCAN_INTERVAL_MS);
|
|
14404
|
+
retryTimer.unref?.();
|
|
14405
|
+
}
|
|
14406
|
+
startSlackRecoveryOutboxWatcher();
|
|
14161
14407
|
function trackPendingMessage(channel, threadTs, messageTs) {
|
|
14162
14408
|
const key = `${channel}:${threadTs}:${messageTs}`;
|
|
14163
14409
|
const timer = setTimeout(async () => {
|
|
14164
14410
|
pendingMessages.delete(key);
|
|
14165
14411
|
try {
|
|
14166
|
-
await fetch("https://slack.com/api/
|
|
14412
|
+
await fetch("https://slack.com/api/chat.postMessage", {
|
|
14167
14413
|
method: "POST",
|
|
14168
14414
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${BOT_TOKEN}` },
|
|
14169
|
-
body: JSON.stringify({
|
|
14415
|
+
body: JSON.stringify({
|
|
14416
|
+
channel,
|
|
14417
|
+
thread_ts: threadTs,
|
|
14418
|
+
text: "Sorry \u2014 I didn't get a response back to you within 5 minutes, so this one fell on the floor. Please re-send if it's still relevant."
|
|
14419
|
+
})
|
|
14170
14420
|
});
|
|
14171
14421
|
} catch {
|
|
14172
14422
|
}
|
|
@@ -14178,10 +14428,12 @@ function trackPendingMessage(channel, threadTs, messageTs) {
|
|
|
14178
14428
|
});
|
|
14179
14429
|
} catch {
|
|
14180
14430
|
}
|
|
14181
|
-
process.stderr.write(`slack-channel: Response timeout for message ${messageTs} in ${channel}
|
|
14431
|
+
process.stderr.write(`slack-channel: Response timeout for message ${redactSlackId(messageTs)} in ${redactSlackId(channel)}
|
|
14182
14432
|
`);
|
|
14433
|
+
clearSlackPendingInboundMarker(channel, threadTs, messageTs);
|
|
14183
14434
|
}, RESPONSE_TIMEOUT_MS);
|
|
14184
14435
|
pendingMessages.set(key, timer);
|
|
14436
|
+
writeSlackPendingInboundMarker(channel, threadTs, messageTs);
|
|
14185
14437
|
}
|
|
14186
14438
|
function clearPendingMessage(channel, threadTs) {
|
|
14187
14439
|
for (const [key, timer] of pendingMessages) {
|
|
@@ -14190,6 +14442,90 @@ function clearPendingMessage(channel, threadTs) {
|
|
|
14190
14442
|
pendingMessages.delete(key);
|
|
14191
14443
|
}
|
|
14192
14444
|
}
|
|
14445
|
+
clearAllSlackPendingMarkersForThread(channel, threadTs);
|
|
14446
|
+
}
|
|
14447
|
+
var RESTART_FLAGS_DIR = join2(homedir2(), ".augmented", "restart-flags");
|
|
14448
|
+
function hashChannelId(id) {
|
|
14449
|
+
return createHash("sha256").update(id).digest("hex").slice(0, 8);
|
|
14450
|
+
}
|
|
14451
|
+
async function postSlackMessage(body) {
|
|
14452
|
+
try {
|
|
14453
|
+
const res = await fetch("https://slack.com/api/chat.postMessage", {
|
|
14454
|
+
method: "POST",
|
|
14455
|
+
headers: {
|
|
14456
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
14457
|
+
Authorization: `Bearer ${BOT_TOKEN}`
|
|
14458
|
+
},
|
|
14459
|
+
body: JSON.stringify(body),
|
|
14460
|
+
// Hard deadline so a hung Slack API can't stall the live Socket Mode
|
|
14461
|
+
// event loop or the manager's restart ack path. Matches the timeout
|
|
14462
|
+
// used elsewhere in this file for chat.postMessage.
|
|
14463
|
+
signal: AbortSignal.timeout(SLACK_DOWNLOAD_TIMEOUT_MS)
|
|
14464
|
+
});
|
|
14465
|
+
return await res.json();
|
|
14466
|
+
} catch (err) {
|
|
14467
|
+
const isTimeout = err.name === "TimeoutError" || err.name === "AbortError";
|
|
14468
|
+
return { ok: false, error: isTimeout ? "timeout" : err.message };
|
|
14469
|
+
}
|
|
14470
|
+
}
|
|
14471
|
+
async function handleRestartCommand(opts) {
|
|
14472
|
+
const codeName = AGENT_CODE_NAME ?? "unknown";
|
|
14473
|
+
try {
|
|
14474
|
+
if (!existsSync(RESTART_FLAGS_DIR)) {
|
|
14475
|
+
mkdirSync2(RESTART_FLAGS_DIR, { recursive: true });
|
|
14476
|
+
}
|
|
14477
|
+
const flagPath = join2(RESTART_FLAGS_DIR, `${codeName}.flag`);
|
|
14478
|
+
const flag = {
|
|
14479
|
+
codeName,
|
|
14480
|
+
source: "slack",
|
|
14481
|
+
ts: Date.now(),
|
|
14482
|
+
reply: {
|
|
14483
|
+
channel: opts.channel,
|
|
14484
|
+
...opts.threadTs ? { thread_ts: opts.threadTs } : {},
|
|
14485
|
+
message_ts: opts.ts
|
|
14486
|
+
}
|
|
14487
|
+
};
|
|
14488
|
+
const tmpPath = `${flagPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
14489
|
+
writeFileSync2(tmpPath, JSON.stringify(flag) + "\n", "utf8");
|
|
14490
|
+
renameSync(tmpPath, flagPath);
|
|
14491
|
+
process.stderr.write(
|
|
14492
|
+
`slack-channel(${codeName}): /restart queued from channel ${hashChannelId(opts.channel)}
|
|
14493
|
+
`
|
|
14494
|
+
);
|
|
14495
|
+
const ack = await postSlackMessage({
|
|
14496
|
+
channel: opts.channel,
|
|
14497
|
+
text: `\u23F3 Restart queued for \`${codeName}\`. The session will reconnect in a few seconds.`,
|
|
14498
|
+
...opts.threadTs ? { thread_ts: opts.threadTs } : {}
|
|
14499
|
+
});
|
|
14500
|
+
if (!ack.ok) {
|
|
14501
|
+
process.stderr.write(
|
|
14502
|
+
`slack-channel(${codeName}): /restart ack send failed: ${ack.error ?? "unknown"}
|
|
14503
|
+
`
|
|
14504
|
+
);
|
|
14505
|
+
}
|
|
14506
|
+
} catch (err) {
|
|
14507
|
+
process.stderr.write(
|
|
14508
|
+
`slack-channel(${codeName}): /restart flag write failed: ${redactAugmentedPaths(err.message)}
|
|
14509
|
+
`
|
|
14510
|
+
);
|
|
14511
|
+
await postSlackMessage({
|
|
14512
|
+
channel: opts.channel,
|
|
14513
|
+
text: `\u274C Failed to queue restart for ${codeName}. Please try again in a few seconds.`,
|
|
14514
|
+
...opts.threadTs ? { thread_ts: opts.threadTs } : {}
|
|
14515
|
+
});
|
|
14516
|
+
}
|
|
14517
|
+
}
|
|
14518
|
+
async function denyUnauthorizedRestart(opts) {
|
|
14519
|
+
const codeName = AGENT_CODE_NAME ?? "unknown";
|
|
14520
|
+
process.stderr.write(
|
|
14521
|
+
`slack-channel(${codeName}): /restart denied \u2014 sender not in SLACK_ALLOWED_USERS, channel ${hashChannelId(opts.channel)}
|
|
14522
|
+
`
|
|
14523
|
+
);
|
|
14524
|
+
await postSlackMessage({
|
|
14525
|
+
channel: opts.channel,
|
|
14526
|
+
text: `\u{1F6AB} \`/restart\` denied \u2014 your Slack user is not in the allowlist for \`${codeName}\`.`,
|
|
14527
|
+
...opts.threadTs ? { thread_ts: opts.threadTs } : {}
|
|
14528
|
+
});
|
|
14193
14529
|
}
|
|
14194
14530
|
var trackedThreads = /* @__PURE__ */ new Map();
|
|
14195
14531
|
var THREAD_STORE_PATH = resolveThreadStorePath();
|
|
@@ -14229,6 +14565,28 @@ if (!BOT_TOKEN || !APP_TOKEN) {
|
|
|
14229
14565
|
);
|
|
14230
14566
|
process.exit(1);
|
|
14231
14567
|
}
|
|
14568
|
+
var slackStderrLogStream = null;
|
|
14569
|
+
if (AGENT_CODE_NAME) {
|
|
14570
|
+
try {
|
|
14571
|
+
const logDir = join2(homedir2(), ".augmented", AGENT_CODE_NAME);
|
|
14572
|
+
mkdirSync2(logDir, { recursive: true });
|
|
14573
|
+
slackStderrLogStream = createWriteStream(join2(logDir, "slack-channel-stderr.log"), {
|
|
14574
|
+
flags: "a",
|
|
14575
|
+
mode: 384
|
|
14576
|
+
});
|
|
14577
|
+
const origWrite = process.stderr.write.bind(process.stderr);
|
|
14578
|
+
process.stderr.write = (chunk, ...rest) => {
|
|
14579
|
+
try {
|
|
14580
|
+
const buf = typeof chunk === "string" ? chunk : String(chunk);
|
|
14581
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
14582
|
+
slackStderrLogStream?.write(`[${ts}] ${buf}${buf.endsWith("\n") ? "" : "\n"}`);
|
|
14583
|
+
} catch {
|
|
14584
|
+
}
|
|
14585
|
+
return origWrite(chunk, ...rest);
|
|
14586
|
+
};
|
|
14587
|
+
} catch {
|
|
14588
|
+
}
|
|
14589
|
+
}
|
|
14232
14590
|
async function getBotUserId() {
|
|
14233
14591
|
try {
|
|
14234
14592
|
const res = await fetch("https://slack.com/api/auth.test", {
|
|
@@ -14264,16 +14622,17 @@ var mcp = new Server(
|
|
|
14264
14622
|
// Attachments rules live near the top because they get chopped otherwise
|
|
14265
14623
|
// and the agent silently loses attachment-handling guidance.
|
|
14266
14624
|
instructions: [
|
|
14267
|
-
|
|
14625
|
+
// Highest-priority lines first — Claude Code truncates this string at
|
|
14626
|
+
// 2048 chars, so anything appended late silently disappears.
|
|
14627
|
+
"CRITICAL: every response to a Slack <channel> tag MUST go through slack.reply. Text in your session WITHOUT a slack.reply call never reaches the user \u2014 the message dies inside the agent process.",
|
|
14628
|
+
'Messages from Slack arrive as <channel source="slack" user="<slack-id>" user_name="<display-name>" channel="..." thread_ts="...">. Pass channel + thread_ts from the tag to slack.reply; always include thread_ts on threaded replies.',
|
|
14268
14629
|
selfIdentityInstruction,
|
|
14269
|
-
"Inbound attachments:
|
|
14270
|
-
"
|
|
14271
|
-
|
|
14272
|
-
"
|
|
14273
|
-
|
|
14274
|
-
|
|
14275
|
-
`When a message in a thread is not addressed to you (different @-mention, conversation between others, auto_followed catch-up): SILENTLY SKIP \u2014 no reaction, no reply, do nothing. Adding \u274C in this case is wrong; it gives the user a misleading "execution failed" signal when in fact you correctly identified the message wasn't yours to handle.`,
|
|
14276
|
-
"To deliver a file, save it under your project dir and call slack.upload_file with path, channel, thread_ts."
|
|
14630
|
+
"Inbound attachments: <channel> `files` is a JSON-serialised array \u2014 JSON.parse it. If an entry has `path`, the image is already downloaded \u2014 Read it directly, do NOT call slack.download_attachment. Use that tool only for entries with `file_id` but NO `path` (PDF, docx, csv): pass file_id + channel verbatim, then Read the returned path. Single-image messages also get a top-level `image_path`. Don't surface internal file-handling errors that don't affect the answer.",
|
|
14631
|
+
"Address users by user_name, never by raw user ID. In multi-participant threads the CURRENT speaker is the one on the latest <channel> tag.",
|
|
14632
|
+
'Mentioned in a channel \u2192 respond in that thread. DM \u2192 respond directly. auto_followed="true" \u2192 only reply if you have something useful to add.',
|
|
14633
|
+
"Reaction taxonomy (use slack.react sparingly \u2014 prefer a reply): \u{1F440} = ack (already auto-added on inbound, do not duplicate); \u2705 = success. NEVER react to signal failure \u2014 users can't tell why something failed from an emoji. On failure, slack.reply with one sentence explaining what went wrong (no stack traces, no secrets).",
|
|
14634
|
+
`When a thread message is NOT addressed to you (different @-mention, side conversation, auto_followed catch-up): SILENTLY SKIP \u2014 no reaction, no reply, no "this wasn't for me" message.`,
|
|
14635
|
+
"To deliver a file: save under your project dir, call slack.upload_file with path + channel + thread_ts."
|
|
14277
14636
|
].join(" ")
|
|
14278
14637
|
}
|
|
14279
14638
|
);
|
|
@@ -14300,13 +14659,13 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
14300
14659
|
},
|
|
14301
14660
|
{
|
|
14302
14661
|
name: "slack.react",
|
|
14303
|
-
description:
|
|
14662
|
+
description: "Add an emoji reaction to a Slack message. Use sparingly \u2014 prefer a text reply. Reaction taxonomy: \u2705 = action completed successfully. NEVER react to signal failure \u2014 users can't tell why it failed from a reaction. On failure, call slack.reply with one sentence explaining what went wrong instead. \u{1F440} (eyes) is already auto-applied on inbound; do not duplicate.",
|
|
14304
14663
|
inputSchema: {
|
|
14305
14664
|
type: "object",
|
|
14306
14665
|
properties: {
|
|
14307
14666
|
channel: { type: "string", description: "Slack channel ID" },
|
|
14308
14667
|
timestamp: { type: "string", description: "Message timestamp to react to" },
|
|
14309
|
-
emoji: { type: "string", description: 'Emoji name without colons (e.g. "white_check_mark"
|
|
14668
|
+
emoji: { type: "string", description: 'Emoji name without colons (e.g. "white_check_mark"). Do NOT use "x" or any other failure emoji \u2014 send a slack.reply explaining the failure instead.' }
|
|
14310
14669
|
},
|
|
14311
14670
|
required: ["channel", "timestamp", "emoji"]
|
|
14312
14671
|
}
|
|
@@ -14872,6 +15231,28 @@ async function connectSocketMode() {
|
|
|
14872
15231
|
const isThreadReply = !!evt.thread_ts && evt.thread_ts !== evt.ts;
|
|
14873
15232
|
const trackTs = evt.thread_ts ?? evt.ts ?? "";
|
|
14874
15233
|
const threadKey = buildThreadKey(evt.channel, trackTs);
|
|
15234
|
+
const rawText = evt.text ?? "";
|
|
15235
|
+
const strippedText = rawText.replace(/^\s*<@[^>]+>\s*/, "").trim();
|
|
15236
|
+
const isRestartCommand = strippedText === "/restart" || strippedText.startsWith("/restart ");
|
|
15237
|
+
if (isRestartCommand) {
|
|
15238
|
+
const senderAllowed = ALLOWED_USERS.size === 0 || evt.user != null && ALLOWED_USERS.has(evt.user);
|
|
15239
|
+
if (!senderAllowed) {
|
|
15240
|
+
await denyUnauthorizedRestart({
|
|
15241
|
+
channel: evt.channel ?? "",
|
|
15242
|
+
threadTs: evt.thread_ts
|
|
15243
|
+
});
|
|
15244
|
+
return;
|
|
15245
|
+
}
|
|
15246
|
+
await handleRestartCommand({
|
|
15247
|
+
channel: evt.channel ?? "",
|
|
15248
|
+
// Only carry thread_ts when the originating message was already
|
|
15249
|
+
// threaded; a top-level command acks in-channel rather than
|
|
15250
|
+
// synthesising a brand-new thread (CodeRabbit feedback).
|
|
15251
|
+
threadTs: evt.thread_ts,
|
|
15252
|
+
ts: evt.ts ?? ""
|
|
15253
|
+
});
|
|
15254
|
+
return;
|
|
15255
|
+
}
|
|
14875
15256
|
if (ALLOWED_USERS.size > 0 && evt.user && !ALLOWED_USERS.has(evt.user)) return;
|
|
14876
15257
|
if (evt.type === "app_mention") {
|
|
14877
15258
|
rememberThread(evt.channel, trackTs, "mentioned");
|