@pinet/slack-bridge 0.1.2 → 0.2.1
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/README.md +59 -32
- package/dist/broker/adapters/slack.d.ts +19 -1
- package/dist/broker/adapters/slack.js +111 -22
- package/dist/broker/client.d.ts +2 -1
- package/dist/broker/client.js +1 -0
- package/dist/broker/socket-server.js +18 -0
- package/dist/deploy-manifest.d.ts +5 -0
- package/dist/deploy-manifest.js +30 -1
- package/dist/follower-runtime.js +5 -1
- package/dist/helpers.d.ts +14 -0
- package/dist/helpers.js +45 -21
- package/dist/index.js +60 -0
- package/dist/pinet-commands.d.ts +6 -1
- package/dist/pinet-commands.js +166 -1
- package/dist/pinet-mesh-ops.d.ts +11 -0
- package/dist/pinet-mesh-ops.js +17 -0
- package/dist/pinet-tools.d.ts +47 -0
- package/dist/pinet-tools.js +506 -38
- package/dist/prompts/broker/tmux.md +2 -2
- package/dist/reaction-triggers.d.ts +1 -0
- package/dist/reaction-triggers.js +26 -15
- package/dist/runtime-agent-context.js +19 -0
- package/dist/runtime-mode.js +7 -1
- package/dist/single-player-runtime.js +22 -26
- package/dist/slack-access.d.ts +11 -0
- package/dist/slack-access.js +30 -0
- package/dist/slack-agents-command.d.ts +19 -0
- package/dist/slack-agents-command.js +90 -0
- package/dist/slack-export.d.ts +1 -1
- package/dist/slack-export.js +6 -4
- package/dist/slack-file-access.d.ts +34 -0
- package/dist/slack-file-access.js +209 -0
- package/dist/slack-message-context.d.ts +0 -1
- package/dist/slack-message-context.js +1 -6
- package/dist/slack-pinet-runtime-adapter.d.ts +4 -2
- package/dist/slack-pinet-runtime-adapter.js +12 -0
- package/dist/slack-tools.d.ts +6 -0
- package/dist/slack-tools.js +290 -36
- package/dist/slack-upload.d.ts +13 -1
- package/dist/slack-upload.js +29 -2
- package/dist/stale-slack-messages.d.ts +12 -0
- package/dist/stale-slack-messages.js +29 -0
- package/dist/subtree-broker-runtime.d.ts +109 -0
- package/dist/subtree-broker-runtime.js +558 -0
- package/manifest.yaml +9 -0
- package/package.json +9 -7
- package/skills/slack-bridge/SKILL.md +60 -1
package/dist/slack-tools.js
CHANGED
|
@@ -7,7 +7,9 @@ import { findSlackPresenceDirectoryUser, formatSlackPresenceLine, formatSlackPre
|
|
|
7
7
|
import { buildSlackThreadExport, filterSlackExportMessagesByRange, parseSlackExportBoundaryTs, } from "./slack-export.js";
|
|
8
8
|
import { normalizeReactionName } from "./reaction-triggers.js";
|
|
9
9
|
import { resolveScheduledWakeupFireAt } from "./scheduled-wakeups.js";
|
|
10
|
-
import {
|
|
10
|
+
import { fetchSlackFileToCache } from "./slack-file-access.js";
|
|
11
|
+
import { extractSlackMessageFileMetadata } from "./slack-message-context.js";
|
|
12
|
+
import { performSlackUpload, performSlackUploads, prepareSlackUpload } from "./slack-upload.js";
|
|
11
13
|
import { TtlCache } from "./ttl-cache.js";
|
|
12
14
|
import { DEFAULT_SLACK_THREAD_STATUS, normalizeSlackThreadStatus, setSlackThreadStatus, SLACK_THREAD_LOADING_MESSAGES, } from "./slack-thread-status.js";
|
|
13
15
|
const SLACK_OUTPUT_OPTION_PARAMETERS = {
|
|
@@ -20,6 +22,14 @@ const SLACK_OUTPUT_OPTION_PARAMETERS = {
|
|
|
20
22
|
})),
|
|
21
23
|
};
|
|
22
24
|
const SLACK_ACTIONS_WITH_FORMAT_ARG = new Set(["export"]);
|
|
25
|
+
const SLACK_LOCAL_FILE_ATTACHMENT_PARAMETERS = Type.Array(Type.Object({
|
|
26
|
+
path: Type.String({
|
|
27
|
+
description: "Local file path to attach. For safety, only files inside the current working directory or system temp directory are allowed.",
|
|
28
|
+
}),
|
|
29
|
+
filename: Type.Optional(Type.String({ description: "Optional Slack filename override" })),
|
|
30
|
+
title: Type.Optional(Type.String({ description: "Optional Slack title" })),
|
|
31
|
+
filetype: Type.Optional(Type.String({ description: "Optional Slack filetype override" })),
|
|
32
|
+
}), { description: "Optional local files to attach to the same Slack message as text." });
|
|
23
33
|
const SLACK_DISPATCHER_EXAMPLES = {
|
|
24
34
|
react: [{ action: "react", args: { emoji: "👀", thread_ts: "1712345678.000100" } }],
|
|
25
35
|
read: [{ action: "read", args: { thread_ts: "1712345678.000100", limit: 20 } }],
|
|
@@ -34,6 +44,16 @@ const SLACK_DISPATCHER_EXAMPLES = {
|
|
|
34
44
|
},
|
|
35
45
|
},
|
|
36
46
|
],
|
|
47
|
+
file: [
|
|
48
|
+
{
|
|
49
|
+
action: "file",
|
|
50
|
+
args: {
|
|
51
|
+
op: "download",
|
|
52
|
+
file_id: "F0123456789",
|
|
53
|
+
thread_ts: "1712345678.000100",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
],
|
|
37
57
|
schedule: [
|
|
38
58
|
{
|
|
39
59
|
action: "schedule",
|
|
@@ -51,6 +71,14 @@ const SLACK_DISPATCHER_EXAMPLES = {
|
|
|
51
71
|
action: "post_channel",
|
|
52
72
|
args: { channel: "#deployments", text: "Deploy complete" },
|
|
53
73
|
},
|
|
74
|
+
{
|
|
75
|
+
action: "post_channel",
|
|
76
|
+
args: {
|
|
77
|
+
channel: "#deployments",
|
|
78
|
+
text: "Deploy evidence attached",
|
|
79
|
+
files: [{ path: "/tmp/evidence.png", filename: "evidence.png" }],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
54
82
|
],
|
|
55
83
|
read_channel: [{ action: "read_channel", args: { channel: "#deployments", limit: 20 } }],
|
|
56
84
|
confirm_action: [
|
|
@@ -167,6 +195,7 @@ function isPinetDeliveryFallbackError(error) {
|
|
|
167
195
|
lower.includes("socket") ||
|
|
168
196
|
lower.includes("no transport source") ||
|
|
169
197
|
lower.includes("no transport channel") ||
|
|
198
|
+
lower.includes("only allows local file paths") ||
|
|
170
199
|
lower.includes("no adapter") ||
|
|
171
200
|
lower.includes("identity is unavailable"));
|
|
172
201
|
}
|
|
@@ -387,7 +416,7 @@ function buildSlackInboxPromptGuidelines() {
|
|
|
387
416
|
"Use slack_send for direct assistant-thread replies. Use the slack dispatcher for non-hot Slack actions such as reactions, reads, uploads, schedules, channel posts, pins, bookmarks, canvases, modals, presence, exports, and confirmations.",
|
|
388
417
|
"Call slack with action='help' for the cold-action catalogue, or action='help' with args.topic for a specific action schema and examples.",
|
|
389
418
|
"Security guardrails may be active for Slack-triggered actions. Cold Slack actions are checked with slack:<action> guardrail names.",
|
|
390
|
-
"
|
|
419
|
+
"Slack emoji reactions are ignored by default. Only treat opt-in structured 'Reaction trigger from Slack:' inbox messages from authorized Pinet threads as user instructions tied to the referenced Slack message or thread; never infer work from a plain emoji reaction alone.",
|
|
391
420
|
];
|
|
392
421
|
}
|
|
393
422
|
function buildSlackSendPromptGuidelines() {
|
|
@@ -477,6 +506,26 @@ function asTrimmedSlackString(value) {
|
|
|
477
506
|
const trimmed = value.trim();
|
|
478
507
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
479
508
|
}
|
|
509
|
+
function extractSlackUploadMessageTs(response, channelId) {
|
|
510
|
+
const files = Array.isArray(response.files) ? response.files : [];
|
|
511
|
+
for (const fileValue of files) {
|
|
512
|
+
const file = asSlackObject(fileValue);
|
|
513
|
+
const shares = asSlackObject(file?.shares);
|
|
514
|
+
for (const shareKind of ["public", "private"]) {
|
|
515
|
+
const shareByChannel = asSlackObject(shares?.[shareKind]);
|
|
516
|
+
const channelShares = shareByChannel?.[channelId];
|
|
517
|
+
if (!Array.isArray(channelShares))
|
|
518
|
+
continue;
|
|
519
|
+
for (const shareValue of channelShares) {
|
|
520
|
+
const share = asSlackObject(shareValue);
|
|
521
|
+
const ts = asTrimmedSlackString(share?.ts);
|
|
522
|
+
if (ts)
|
|
523
|
+
return ts;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
480
529
|
function extractSlackCanvasPermalink(response) {
|
|
481
530
|
const direct = asTrimmedSlackString(response.permalink) ??
|
|
482
531
|
asTrimmedSlackString(response.url) ??
|
|
@@ -510,8 +559,39 @@ function isPiAgentSlackMessage(message, botUserId, agentOwnerToken) {
|
|
|
510
559
|
}
|
|
511
560
|
return eventPayload.agent_owner === agentOwnerToken;
|
|
512
561
|
}
|
|
562
|
+
/**
|
|
563
|
+
* Build the canonical guarded action string for a Slack delete.
|
|
564
|
+
*
|
|
565
|
+
* This must be derived from RESOLVED values (resolved channel ID, normalized
|
|
566
|
+
* ts list) — never raw caller params — so that the string quoted in guardrail
|
|
567
|
+
* errors, registered via confirm_action, and re-checked on the post-approval
|
|
568
|
+
* retry always agree regardless of how the caller phrased the request (#814).
|
|
569
|
+
*/
|
|
513
570
|
function summarizeSlackDeleteAction(input) {
|
|
514
|
-
return `channel=${input.channel
|
|
571
|
+
return `channel=${input.channel} | thread_ts=${input.threadTs ?? ""} | ts=${input.tsList.join(",")} | thread=${input.thread ?? false}`;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Normalize the delete `ts` input into a deterministic target list.
|
|
575
|
+
*
|
|
576
|
+
* Multiple comma/whitespace-separated timestamps are allowed for single-message
|
|
577
|
+
* deletes so one explicit approval can cover one batch of bot-authored cleanup
|
|
578
|
+
* messages in the same thread (#814). The list is trimmed, deduplicated, and
|
|
579
|
+
* sorted so the canonical action string is stable across caller phrasings.
|
|
580
|
+
*/
|
|
581
|
+
function parseSlackDeleteTsInput(ts, deleteThread) {
|
|
582
|
+
const tsList = [
|
|
583
|
+
...new Set(ts
|
|
584
|
+
.split(/[\s,]+/)
|
|
585
|
+
.map((entry) => entry.trim())
|
|
586
|
+
.filter((entry) => entry.length > 0)),
|
|
587
|
+
].sort();
|
|
588
|
+
if (tsList.length === 0) {
|
|
589
|
+
throw new Error("ts is required.");
|
|
590
|
+
}
|
|
591
|
+
if (deleteThread && tsList.length > 1) {
|
|
592
|
+
throw new Error("When thread=true, ts must be a single thread root timestamp.");
|
|
593
|
+
}
|
|
594
|
+
return tsList;
|
|
515
595
|
}
|
|
516
596
|
export function registerSlackTools(pi, deps) {
|
|
517
597
|
const { getBotToken, getDefaultChannel, getSecurityPrompt, inbox, slack, getAgentName, getAgentEmoji, getAgentOwnerToken, getLastDmChannel, updateBadge, resolveUser, threadContext, resolveChannel, rememberChannel, requireToolPolicy: requireToolPolicyForName, registerConfirmationRequest, getBotUserId, pinetDelivery, } = deps;
|
|
@@ -553,6 +633,7 @@ export function registerSlackTools(pi, deps) {
|
|
|
553
633
|
channel: input.channel,
|
|
554
634
|
text: input.text,
|
|
555
635
|
...(blocks ? { blocks } : {}),
|
|
636
|
+
...(input.files ? { files: input.files } : {}),
|
|
556
637
|
});
|
|
557
638
|
return {
|
|
558
639
|
threadTs: input.threadTs,
|
|
@@ -570,6 +651,34 @@ export function registerSlackTools(pi, deps) {
|
|
|
570
651
|
fallbackReason = getErrorMessage(error);
|
|
571
652
|
}
|
|
572
653
|
}
|
|
654
|
+
if (input.files && input.files.length > 0) {
|
|
655
|
+
if (blocks && blocks.length > 0) {
|
|
656
|
+
throw new Error("Slack text+file replies use Slack's external upload flow, which does not support Block Kit blocks in the same upload message. Omit blocks or send a separate block-only message.");
|
|
657
|
+
}
|
|
658
|
+
const uploads = await Promise.all(input.files.map((file) => prepareSlackUpload({
|
|
659
|
+
path: file.path,
|
|
660
|
+
...(file.filename ? { filename: file.filename } : {}),
|
|
661
|
+
...(file.title ? { title: file.title } : {}),
|
|
662
|
+
...(file.filetype ? { filetype: file.filetype } : {}),
|
|
663
|
+
}, process.cwd(), os.tmpdir())));
|
|
664
|
+
const uploadResult = await performSlackUploads({
|
|
665
|
+
uploads,
|
|
666
|
+
channelId: input.channel,
|
|
667
|
+
...(input.threadTs ? { threadTs: input.threadTs } : {}),
|
|
668
|
+
initialComment: input.text,
|
|
669
|
+
slack,
|
|
670
|
+
token: getBotToken(),
|
|
671
|
+
});
|
|
672
|
+
const uploadTs = extractSlackUploadMessageTs(uploadResult.response, input.channel);
|
|
673
|
+
return {
|
|
674
|
+
...(uploadTs ? { ts: uploadTs } : {}),
|
|
675
|
+
threadTs: input.threadTs ?? uploadTs,
|
|
676
|
+
channel: input.channel,
|
|
677
|
+
blocksCount: blocks?.length ?? 0,
|
|
678
|
+
delivery: "slack",
|
|
679
|
+
...(fallbackReason ? { fallbackReason } : {}),
|
|
680
|
+
};
|
|
681
|
+
}
|
|
573
682
|
const body = {
|
|
574
683
|
channel: input.channel,
|
|
575
684
|
text: input.text,
|
|
@@ -584,9 +693,10 @@ export function registerSlackTools(pi, deps) {
|
|
|
584
693
|
if (input.threadTs)
|
|
585
694
|
body.thread_ts = input.threadTs;
|
|
586
695
|
const response = await slack("chat.postMessage", getBotToken(), body);
|
|
587
|
-
const
|
|
696
|
+
const message = isRecord(response.message) ? response.message : null;
|
|
697
|
+
const ts = typeof message?.ts === "string" ? message.ts : undefined;
|
|
588
698
|
return {
|
|
589
|
-
ts,
|
|
699
|
+
...(ts ? { ts } : {}),
|
|
590
700
|
channel: input.channel,
|
|
591
701
|
blocksCount: blocks?.length ?? 0,
|
|
592
702
|
delivery: "slack",
|
|
@@ -650,7 +760,7 @@ export function registerSlackTools(pi, deps) {
|
|
|
650
760
|
pi.registerTool({
|
|
651
761
|
name: "slack",
|
|
652
762
|
label: "Slack",
|
|
653
|
-
description: "Dispatcher for non-hot Slack actions: react, read, upload, schedule, presence, export, post_channel, read_channel, confirm_action, delete, pin, bookmark, create_channel, project_create, canvas_comments_read, canvas_create, canvas_update, modal_open, modal_push, modal_update, and help. Use slack_inbox and slack_send for hot-path inbox/reply work.",
|
|
763
|
+
description: "Dispatcher for non-hot Slack actions: react, read, upload, file, schedule, presence, export, post_channel, read_channel, confirm_action, delete, pin, bookmark, create_channel, project_create, canvas_comments_read, canvas_create, canvas_update, modal_open, modal_push, modal_update, and help. Use slack_inbox and slack_send for hot-path inbox/reply work.",
|
|
654
764
|
promptSnippet: "Run non-hot Slack actions through a compact dispatcher. Use action='help' for the action catalogue or args.topic for a specific schema. Defaults to compact cli output; pass args.format='json' (or args.response_format='json' when the action owns format) or args.full=true for structured/full details.",
|
|
655
765
|
promptGuidelines: [
|
|
656
766
|
"Use slack_inbox and slack_send for the hot path. Use this dispatcher for every other Slack action.",
|
|
@@ -660,7 +770,7 @@ export function registerSlackTools(pi, deps) {
|
|
|
660
770
|
],
|
|
661
771
|
parameters: Type.Object({
|
|
662
772
|
action: Type.String({
|
|
663
|
-
description: "Action name: help | react | read | upload | schedule | presence | export | post_channel | read_channel | confirm_action | delete | pin | bookmark | create_channel | project_create | canvas_comments_read | canvas_create | canvas_update | modal_open | modal_push | modal_update",
|
|
773
|
+
description: "Action name: help | react | read | upload | file | schedule | presence | export | post_channel | read_channel | confirm_action | delete | pin | bookmark | create_channel | project_create | canvas_comments_read | canvas_create | canvas_update | modal_open | modal_push | modal_update",
|
|
664
774
|
}),
|
|
665
775
|
args: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
|
|
666
776
|
description: "Action arguments. Call slack with action='help' and args.topic='<action>' for the exact JSON schema and examples. Add format='cli'|'json' and full=true for explicit presentation control; use response_format when the action already has a format field.",
|
|
@@ -862,20 +972,20 @@ export function registerSlackTools(pi, deps) {
|
|
|
862
972
|
return messages;
|
|
863
973
|
}
|
|
864
974
|
async function resolveSlackDeleteTargets(input) {
|
|
865
|
-
const channelId =
|
|
866
|
-
const targetTs = input.
|
|
975
|
+
const { channelId } = input;
|
|
976
|
+
const targetTs = input.tsList[0];
|
|
867
977
|
if (!targetTs) {
|
|
868
978
|
throw new Error("ts is required.");
|
|
869
979
|
}
|
|
870
980
|
if (!input.thread) {
|
|
871
|
-
return { channelId, messageTsList:
|
|
981
|
+
return { channelId, messageTsList: input.tsList };
|
|
872
982
|
}
|
|
873
983
|
const messages = await fetchSlackThreadMessages(channelId, targetTs, undefined, undefined, true);
|
|
874
984
|
const threadRootTs = messages.length > 0 && typeof messages[0]?.ts === "string"
|
|
875
985
|
? messages[0].ts
|
|
876
986
|
: undefined;
|
|
877
987
|
if (!threadRootTs) {
|
|
878
|
-
throw new Error(`Slack did not return a thread rooted at ${targetTs} in channel ${
|
|
988
|
+
throw new Error(`Slack did not return a thread rooted at ${targetTs} in channel ${channelId}.`);
|
|
879
989
|
}
|
|
880
990
|
if (threadRootTs !== targetTs) {
|
|
881
991
|
throw new Error("When thread=true, ts must be the thread root timestamp.");
|
|
@@ -929,16 +1039,12 @@ export function registerSlackTools(pi, deps) {
|
|
|
929
1039
|
authorName,
|
|
930
1040
|
text: typeof message.text === "string" ? message.text : "",
|
|
931
1041
|
files: rawFiles.map((file) => ({
|
|
1042
|
+
id: typeof file.id === "string" ? file.id : undefined,
|
|
932
1043
|
name: typeof file.name === "string" ? file.name : undefined,
|
|
933
1044
|
title: typeof file.title === "string" ? file.title : undefined,
|
|
934
1045
|
mimetype: typeof file.mimetype === "string" ? file.mimetype : undefined,
|
|
935
1046
|
filetype: typeof file.filetype === "string" ? file.filetype : undefined,
|
|
936
1047
|
permalink: typeof file.permalink === "string" ? file.permalink : undefined,
|
|
937
|
-
urlPrivate: typeof file.url_private_download === "string"
|
|
938
|
-
? file.url_private_download
|
|
939
|
-
: typeof file.url_private === "string"
|
|
940
|
-
? file.url_private
|
|
941
|
-
: undefined,
|
|
942
1048
|
preview: typeof file.preview === "string" ? file.preview : undefined,
|
|
943
1049
|
})),
|
|
944
1050
|
};
|
|
@@ -1328,9 +1434,10 @@ export function registerSlackTools(pi, deps) {
|
|
|
1328
1434
|
blocks: Type.Optional(Type.Array(Type.Record(Type.String(), Type.Unknown()), {
|
|
1329
1435
|
description: "Optional Slack Block Kit blocks JSON array",
|
|
1330
1436
|
})),
|
|
1437
|
+
files: Type.Optional(SLACK_LOCAL_FILE_ATTACHMENT_PARAMETERS),
|
|
1331
1438
|
}),
|
|
1332
1439
|
async execute(_id, params) {
|
|
1333
|
-
requireToolPolicy("slack_send", params.thread_ts, `thread_ts=${params.thread_ts ?? ""} | text=${params.text} | blocks=${summarizeSlackBlocksForPolicy(params.blocks)}`);
|
|
1440
|
+
requireToolPolicy("slack_send", params.thread_ts, `thread_ts=${params.thread_ts ?? ""} | text=${params.text} | blocks=${summarizeSlackBlocksForPolicy(params.blocks)} | files=${Array.isArray(params.files) ? params.files.length : 0}`);
|
|
1334
1441
|
const channel = await resolveSlackSendChannel(params.thread_ts);
|
|
1335
1442
|
if (!channel) {
|
|
1336
1443
|
throw new Error(params.thread_ts
|
|
@@ -1342,6 +1449,7 @@ export function registerSlackTools(pi, deps) {
|
|
|
1342
1449
|
text: params.text,
|
|
1343
1450
|
...(params.thread_ts ? { threadTs: params.thread_ts } : {}),
|
|
1344
1451
|
...(params.blocks ? { blocks: params.blocks } : {}),
|
|
1452
|
+
...(params.files ? { files: params.files } : {}),
|
|
1345
1453
|
});
|
|
1346
1454
|
const ts = delivery.ts;
|
|
1347
1455
|
const threadTs = params.thread_ts ?? delivery.threadTs;
|
|
@@ -1368,6 +1476,7 @@ export function registerSlackTools(pi, deps) {
|
|
|
1368
1476
|
channel,
|
|
1369
1477
|
blocksCount: delivery.blocksCount,
|
|
1370
1478
|
delivery: delivery.delivery,
|
|
1479
|
+
filesCount: Array.isArray(params.files) ? params.files.length : 0,
|
|
1371
1480
|
...(delivery.adapter ? { adapter: delivery.adapter } : {}),
|
|
1372
1481
|
...(delivery.messageId ? { messageId: delivery.messageId } : {}),
|
|
1373
1482
|
...(delivery.fallbackReason ? { fallbackReason: delivery.fallbackReason } : {}),
|
|
@@ -1491,6 +1600,50 @@ export function registerSlackTools(pi, deps) {
|
|
|
1491
1600
|
};
|
|
1492
1601
|
},
|
|
1493
1602
|
});
|
|
1603
|
+
registerSlackAction({
|
|
1604
|
+
name: "slack_file",
|
|
1605
|
+
label: "Slack File Access",
|
|
1606
|
+
description: "Download a Slack-hosted file from a known file ID, optionally verified against a thread/message, into controlled local temp cache storage without exposing private Slack URLs.",
|
|
1607
|
+
promptSnippet: "Use slack action=file with op=download to turn a Slack-hosted file_id from slackFiles metadata into a safe local file descriptor/path. Private Slack download URLs are never returned.",
|
|
1608
|
+
parameters: Type.Object({
|
|
1609
|
+
op: Type.String({ description: "Operation. Currently only download is supported." }),
|
|
1610
|
+
file_id: Type.String({ description: "Slack file ID, for example F0123456789." }),
|
|
1611
|
+
thread_ts: Type.Optional(Type.String({
|
|
1612
|
+
description: "Optional Slack thread timestamp. When provided, the file must appear in that thread.",
|
|
1613
|
+
})),
|
|
1614
|
+
message_ts: Type.Optional(Type.String({
|
|
1615
|
+
description: "Optional Slack message timestamp inside the thread. When provided, the file must appear on that exact message.",
|
|
1616
|
+
})),
|
|
1617
|
+
channel: Type.Optional(Type.String({
|
|
1618
|
+
description: "Optional channel name or ID. Omit when thread_ts is tracked by the Slack bridge.",
|
|
1619
|
+
})),
|
|
1620
|
+
}),
|
|
1621
|
+
async execute(_id, params) {
|
|
1622
|
+
if (params.op !== "download") {
|
|
1623
|
+
throw new Error("slack file op must be download.");
|
|
1624
|
+
}
|
|
1625
|
+
requireToolPolicy("slack_file", params.thread_ts, `op=download | file_id=${params.file_id} | thread_ts=${params.thread_ts ?? ""} | message_ts=${params.message_ts ?? ""} | channel=${params.channel ?? ""}`);
|
|
1626
|
+
const channelId = params.thread_ts
|
|
1627
|
+
? await resolveSlackTargetChannel(params.thread_ts, params.channel)
|
|
1628
|
+
: params.channel
|
|
1629
|
+
? await resolveChannel(params.channel)
|
|
1630
|
+
: undefined;
|
|
1631
|
+
const descriptor = await fetchSlackFileToCache(params.file_id, {
|
|
1632
|
+
...(channelId ? { channelId } : {}),
|
|
1633
|
+
...(params.thread_ts ? { threadTs: params.thread_ts } : {}),
|
|
1634
|
+
...(params.message_ts ? { messageTs: params.message_ts } : {}),
|
|
1635
|
+
}, { slack, token: getBotToken() });
|
|
1636
|
+
return {
|
|
1637
|
+
content: [
|
|
1638
|
+
{
|
|
1639
|
+
type: "text",
|
|
1640
|
+
text: `Downloaded Slack file ${descriptor.fileId} to ${descriptor.path}.`,
|
|
1641
|
+
},
|
|
1642
|
+
],
|
|
1643
|
+
details: descriptor,
|
|
1644
|
+
};
|
|
1645
|
+
},
|
|
1646
|
+
});
|
|
1494
1647
|
registerSlackAction({
|
|
1495
1648
|
name: "slack_read",
|
|
1496
1649
|
label: "Slack Read",
|
|
@@ -1499,10 +1652,13 @@ export function registerSlackTools(pi, deps) {
|
|
|
1499
1652
|
parameters: Type.Object({
|
|
1500
1653
|
thread_ts: Type.String({ description: "Thread to read." }),
|
|
1501
1654
|
limit: Type.Optional(Type.Number({ description: "Max messages (default 20)" })),
|
|
1655
|
+
download_files: Type.Optional(Type.Boolean({
|
|
1656
|
+
description: "Download attached Slack-hosted files to the local temp cache and return safe descriptors. Defaults to true.",
|
|
1657
|
+
})),
|
|
1502
1658
|
...SLACK_OUTPUT_OPTION_PARAMETERS,
|
|
1503
1659
|
}),
|
|
1504
1660
|
async execute(_id, params) {
|
|
1505
|
-
requireToolPolicy("slack_read", params.thread_ts, `thread_ts=${params.thread_ts} | limit=${params.limit ?? 20}`);
|
|
1661
|
+
requireToolPolicy("slack_read", params.thread_ts, `thread_ts=${params.thread_ts} | limit=${params.limit ?? 20} | download_files=${params.download_files !== false}`);
|
|
1506
1662
|
const channel = (await resolveTrackedThreadChannel(params.thread_ts)) ?? getLastDmChannel();
|
|
1507
1663
|
if (!channel) {
|
|
1508
1664
|
throw new Error("Unknown thread.");
|
|
@@ -1514,37 +1670,126 @@ export function registerSlackTools(pi, deps) {
|
|
|
1514
1670
|
});
|
|
1515
1671
|
const messages = response.messages;
|
|
1516
1672
|
const full = params.full === true;
|
|
1673
|
+
const shouldDownloadFiles = params.download_files !== false;
|
|
1674
|
+
const downloadFile = async (fileId, messageTs) => fetchSlackFileToCache(fileId, { channelId: channel, threadTs: params.thread_ts, messageTs }, { slack, token: getBotToken() });
|
|
1517
1675
|
const formattedMessages = await Promise.all(messages.map(async (message) => {
|
|
1518
1676
|
const userId = message.user;
|
|
1519
1677
|
const name = userId ? await resolveUser(userId) : "bot";
|
|
1520
1678
|
const text = message.text ?? "";
|
|
1521
1679
|
const ts = message.ts;
|
|
1522
|
-
|
|
1680
|
+
const files = await Promise.all(extractSlackMessageFileMetadata(message.files).map(async (file) => {
|
|
1681
|
+
const fileId = file.id;
|
|
1682
|
+
if (!fileId) {
|
|
1683
|
+
return {
|
|
1684
|
+
messageTs: ts,
|
|
1685
|
+
...(file.name ? { filename: file.name } : {}),
|
|
1686
|
+
...(file.mimetype ? { mimetype: file.mimetype } : {}),
|
|
1687
|
+
...(file.filetype ? { filetype: file.filetype } : {}),
|
|
1688
|
+
...(file.prettyType ? { prettyType: file.prettyType } : {}),
|
|
1689
|
+
...(file.size != null ? { size: file.size } : {}),
|
|
1690
|
+
downloadStatus: "metadata-only",
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
const base = {
|
|
1694
|
+
fileId,
|
|
1695
|
+
messageTs: ts,
|
|
1696
|
+
...(file.name ? { filename: file.name } : {}),
|
|
1697
|
+
...(file.mimetype ? { mimetype: file.mimetype } : {}),
|
|
1698
|
+
...(file.filetype ? { filetype: file.filetype } : {}),
|
|
1699
|
+
...(file.prettyType ? { prettyType: file.prettyType } : {}),
|
|
1700
|
+
...(file.size != null ? { size: file.size } : {}),
|
|
1701
|
+
};
|
|
1702
|
+
if (!shouldDownloadFiles) {
|
|
1703
|
+
return {
|
|
1704
|
+
...base,
|
|
1705
|
+
downloadStatus: "metadata-only",
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
try {
|
|
1709
|
+
const descriptor = await downloadFile(fileId, ts);
|
|
1710
|
+
return {
|
|
1711
|
+
...base,
|
|
1712
|
+
filename: descriptor.filename,
|
|
1713
|
+
...(descriptor.mimetype ? { mimetype: descriptor.mimetype } : {}),
|
|
1714
|
+
...(descriptor.filetype ? { filetype: descriptor.filetype } : {}),
|
|
1715
|
+
...(descriptor.prettyType ? { prettyType: descriptor.prettyType } : {}),
|
|
1716
|
+
size: descriptor.size,
|
|
1717
|
+
path: descriptor.path,
|
|
1718
|
+
sha256: descriptor.sha256,
|
|
1719
|
+
cacheDir: descriptor.cacheDir,
|
|
1720
|
+
expiresAt: descriptor.expiresAt,
|
|
1721
|
+
downloadStatus: "downloaded",
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
catch (error) {
|
|
1725
|
+
return {
|
|
1726
|
+
...base,
|
|
1727
|
+
downloadStatus: "failed",
|
|
1728
|
+
error: getErrorMessage(error),
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
}));
|
|
1732
|
+
return { ts, name, text, preview: truncateSlackText(text), files };
|
|
1523
1733
|
}));
|
|
1524
|
-
const lines = formattedMessages.
|
|
1525
|
-
|
|
1526
|
-
|
|
1734
|
+
const lines = formattedMessages.flatMap((message) => {
|
|
1735
|
+
const messageLine = full
|
|
1736
|
+
? `[${message.ts}] ${message.name}: ${message.text}`
|
|
1737
|
+
: `[${message.ts}] ${message.name}: ${message.preview}`;
|
|
1738
|
+
const fileLines = message.files.map((file) => {
|
|
1739
|
+
const filename = file.filename ? ` ${file.filename}` : "";
|
|
1740
|
+
const type = file.prettyType ?? file.filetype ?? file.mimetype ?? "file";
|
|
1741
|
+
if (file.downloadStatus === "downloaded") {
|
|
1742
|
+
return ` [file downloaded] ${file.fileId}${filename} (${type}) -> ${file.path}`;
|
|
1743
|
+
}
|
|
1744
|
+
if (file.downloadStatus === "failed") {
|
|
1745
|
+
return ` [file metadata] ${file.fileId}${filename} (${type}) download failed: ${file.error}`;
|
|
1746
|
+
}
|
|
1747
|
+
return ` [file metadata] ${"fileId" in file ? file.fileId : "unknown-file"}${filename} (${type})`;
|
|
1748
|
+
});
|
|
1749
|
+
return [messageLine, ...fileLines];
|
|
1750
|
+
});
|
|
1751
|
+
const downloadedFilesCount = formattedMessages.reduce((count, message) => count + message.files.filter((file) => file.downloadStatus === "downloaded").length, 0);
|
|
1752
|
+
const failedFilesCount = formattedMessages.reduce((count, message) => count + message.files.filter((file) => file.downloadStatus === "failed").length, 0);
|
|
1527
1753
|
if (!full && messages.length > 0) {
|
|
1528
1754
|
lines.push("", "Use args.full=true for exact message text.");
|
|
1529
1755
|
}
|
|
1756
|
+
if (downloadedFilesCount > 0 || failedFilesCount > 0) {
|
|
1757
|
+
lines.push("", `File attachments: ${downloadedFilesCount} downloaded to the local temp cache${failedFilesCount > 0 ? `; ${failedFilesCount} failed and returned metadata only` : ""}.`);
|
|
1758
|
+
}
|
|
1530
1759
|
return {
|
|
1531
1760
|
content: [{ type: "text", text: lines.join("\n") || "(no messages)" }],
|
|
1532
1761
|
details: full
|
|
1533
|
-
? {
|
|
1762
|
+
? {
|
|
1763
|
+
count: messages.length,
|
|
1764
|
+
downloadedFilesCount,
|
|
1765
|
+
failedFilesCount,
|
|
1766
|
+
messages: formattedMessages.map((message) => ({
|
|
1767
|
+
ts: message.ts,
|
|
1768
|
+
user: message.name,
|
|
1769
|
+
text: message.text,
|
|
1770
|
+
files: message.files,
|
|
1771
|
+
})),
|
|
1772
|
+
}
|
|
1534
1773
|
: {
|
|
1535
1774
|
count: messages.length,
|
|
1775
|
+
downloadedFilesCount,
|
|
1776
|
+
failedFilesCount,
|
|
1536
1777
|
messages: formattedMessages.map((message) => ({
|
|
1537
1778
|
ts: message.ts,
|
|
1538
1779
|
user: message.name,
|
|
1539
1780
|
preview: message.preview,
|
|
1781
|
+
files: message.files,
|
|
1540
1782
|
})),
|
|
1541
1783
|
},
|
|
1542
1784
|
fullDetails: {
|
|
1543
1785
|
count: messages.length,
|
|
1786
|
+
downloadedFilesCount,
|
|
1787
|
+
failedFilesCount,
|
|
1544
1788
|
messages: formattedMessages.map((message) => ({
|
|
1545
1789
|
ts: message.ts,
|
|
1546
1790
|
user: message.name,
|
|
1547
1791
|
text: message.text,
|
|
1792
|
+
files: message.files,
|
|
1548
1793
|
})),
|
|
1549
1794
|
},
|
|
1550
1795
|
};
|
|
@@ -1903,9 +2148,10 @@ export function registerSlackTools(pi, deps) {
|
|
|
1903
2148
|
blocks: Type.Optional(Type.Array(Type.Record(Type.String(), Type.Unknown()), {
|
|
1904
2149
|
description: "Optional Slack Block Kit blocks JSON array",
|
|
1905
2150
|
})),
|
|
2151
|
+
files: Type.Optional(SLACK_LOCAL_FILE_ATTACHMENT_PARAMETERS),
|
|
1906
2152
|
}),
|
|
1907
2153
|
async execute(_id, params) {
|
|
1908
|
-
requireToolPolicy("slack_post_channel", params.thread_ts, `channel=${params.channel ?? getDefaultChannel() ?? ""} | thread_ts=${params.thread_ts ?? ""} | text=${params.text} | blocks=${summarizeSlackBlocksForPolicy(params.blocks)}`);
|
|
2154
|
+
requireToolPolicy("slack_post_channel", params.thread_ts, `channel=${params.channel ?? getDefaultChannel() ?? ""} | thread_ts=${params.thread_ts ?? ""} | text=${params.text} | blocks=${summarizeSlackBlocksForPolicy(params.blocks)} | files=${Array.isArray(params.files) ? params.files.length : 0}`);
|
|
1909
2155
|
const resolvedThreadChannel = await resolveTrackedThreadChannel(params.thread_ts);
|
|
1910
2156
|
const channelInput = params.channel ?? getDefaultChannel();
|
|
1911
2157
|
let channelId = params.channel ? await resolveChannel(params.channel) : resolvedThreadChannel;
|
|
@@ -1920,6 +2166,7 @@ export function registerSlackTools(pi, deps) {
|
|
|
1920
2166
|
text: params.text,
|
|
1921
2167
|
...(params.thread_ts ? { threadTs: params.thread_ts } : {}),
|
|
1922
2168
|
...(params.blocks ? { blocks: params.blocks } : {}),
|
|
2169
|
+
...(params.files ? { files: params.files } : {}),
|
|
1923
2170
|
});
|
|
1924
2171
|
const ts = delivery.ts;
|
|
1925
2172
|
const threadTs = params.thread_ts ?? delivery.threadTs;
|
|
@@ -1944,6 +2191,7 @@ export function registerSlackTools(pi, deps) {
|
|
|
1944
2191
|
channel: channelId,
|
|
1945
2192
|
blocksCount: delivery.blocksCount,
|
|
1946
2193
|
delivery: delivery.delivery,
|
|
2194
|
+
filesCount: Array.isArray(params.files) ? params.files.length : 0,
|
|
1947
2195
|
...(delivery.adapter ? { adapter: delivery.adapter } : {}),
|
|
1948
2196
|
...(delivery.messageId ? { messageId: delivery.messageId } : {}),
|
|
1949
2197
|
...(delivery.fallbackReason ? { fallbackReason: delivery.fallbackReason } : {}),
|
|
@@ -1958,7 +2206,7 @@ export function registerSlackTools(pi, deps) {
|
|
|
1958
2206
|
promptSnippet: "Delete a bot-posted Slack message. This is destructive — set confirm=true, and prefer explicit approval before deleting whole threads.",
|
|
1959
2207
|
parameters: Type.Object({
|
|
1960
2208
|
ts: Type.String({
|
|
1961
|
-
description: "Timestamp (ts) of the message to delete. When thread=true, this must be
|
|
2209
|
+
description: "Timestamp (ts) of the message to delete, or a comma-separated list of bot-posted message timestamps to delete in one confirmed batch. When thread=true, this must be a single thread root timestamp.",
|
|
1962
2210
|
}),
|
|
1963
2211
|
channel: Type.Optional(Type.String({
|
|
1964
2212
|
description: "Channel name or ID. Omit to use the current thread channel, active DM, or defaultChannel.",
|
|
@@ -1977,18 +2225,22 @@ export function registerSlackTools(pi, deps) {
|
|
|
1977
2225
|
if (params.confirm !== true) {
|
|
1978
2226
|
throw new Error("Deleting Slack messages is irreversible. Re-run with confirm=true once you've verified the target.");
|
|
1979
2227
|
}
|
|
2228
|
+
const deleteThread = params.thread === true;
|
|
2229
|
+
const requestedTsList = parseSlackDeleteTsInput(params.ts, deleteThread);
|
|
2230
|
+
// Resolve the actual deletion channel BEFORE the guardrail check so the
|
|
2231
|
+
// canonical action quoted in errors, registered via confirm_action, and
|
|
2232
|
+
// enforced on the post-approval retry is identical regardless of how the
|
|
2233
|
+
// caller phrased channel/ts (#814).
|
|
2234
|
+
const channelId = await resolveSlackTargetChannel(params.thread_ts, params.channel);
|
|
1980
2235
|
requireToolPolicy("slack_delete", params.thread_ts, summarizeSlackDeleteAction({
|
|
1981
|
-
channel:
|
|
1982
|
-
defaultChannel: getDefaultChannel(),
|
|
2236
|
+
channel: channelId,
|
|
1983
2237
|
threadTs: params.thread_ts,
|
|
1984
|
-
|
|
1985
|
-
thread:
|
|
2238
|
+
tsList: requestedTsList,
|
|
2239
|
+
thread: deleteThread,
|
|
1986
2240
|
}));
|
|
1987
|
-
const
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
threadTs: params.thread_ts,
|
|
1991
|
-
ts: params.ts,
|
|
2241
|
+
const { messageTsList } = await resolveSlackDeleteTargets({
|
|
2242
|
+
channelId,
|
|
2243
|
+
tsList: requestedTsList,
|
|
1992
2244
|
thread: deleteThread,
|
|
1993
2245
|
});
|
|
1994
2246
|
for (const messageTs of messageTsList) {
|
|
@@ -1997,7 +2249,7 @@ export function registerSlackTools(pi, deps) {
|
|
|
1997
2249
|
ts: messageTs,
|
|
1998
2250
|
});
|
|
1999
2251
|
}
|
|
2000
|
-
const targetTs =
|
|
2252
|
+
const targetTs = requestedTsList[0];
|
|
2001
2253
|
const deletedCount = messageTsList.length;
|
|
2002
2254
|
const channelLabel = params.channel ?? channelId;
|
|
2003
2255
|
return {
|
|
@@ -2006,7 +2258,9 @@ export function registerSlackTools(pi, deps) {
|
|
|
2006
2258
|
type: "text",
|
|
2007
2259
|
text: deleteThread
|
|
2008
2260
|
? `Deleted thread rooted at ${targetTs} in channel ${channelLabel} (${deletedCount} message${deletedCount === 1 ? "" : "s"}).`
|
|
2009
|
-
:
|
|
2261
|
+
: deletedCount === 1
|
|
2262
|
+
? `Deleted message ${targetTs} from channel ${channelLabel}.`
|
|
2263
|
+
: `Deleted ${deletedCount} messages (${messageTsList.join(", ")}) from channel ${channelLabel}.`,
|
|
2010
2264
|
},
|
|
2011
2265
|
],
|
|
2012
2266
|
details: {
|
package/dist/slack-upload.d.ts
CHANGED
|
@@ -26,11 +26,22 @@ export interface PerformSlackUploadOptions extends SlackUploadDeps {
|
|
|
26
26
|
upload: PreparedSlackUpload;
|
|
27
27
|
channelId: string;
|
|
28
28
|
threadTs?: string;
|
|
29
|
+
initialComment?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface PerformSlackUploadsOptions extends SlackUploadDeps {
|
|
32
|
+
uploads: readonly PreparedSlackUpload[];
|
|
33
|
+
channelId: string;
|
|
34
|
+
threadTs?: string;
|
|
35
|
+
initialComment?: string;
|
|
29
36
|
}
|
|
30
37
|
export interface CompletedSlackUpload {
|
|
31
38
|
fileId: string;
|
|
32
39
|
response: SlackResult;
|
|
33
40
|
}
|
|
41
|
+
export interface CompletedSlackUploads {
|
|
42
|
+
fileIds: string[];
|
|
43
|
+
response: SlackResult;
|
|
44
|
+
}
|
|
34
45
|
interface PrepareSlackUploadFs {
|
|
35
46
|
readFileImpl?: typeof readFile;
|
|
36
47
|
realpathImpl?: typeof realpath;
|
|
@@ -45,5 +56,6 @@ export declare function chooseSlackSnippetType(upload: {
|
|
|
45
56
|
}): string | undefined;
|
|
46
57
|
export declare function resolveSlackUploadPath(inputPath: string, cwd: string, tmpdir: string, fsDeps?: PrepareSlackUploadFs): Promise<string>;
|
|
47
58
|
export declare function prepareSlackUpload(params: SlackUploadParams, cwd: string, tmpdir: string, fsDeps?: PrepareSlackUploadFs): Promise<PreparedSlackUpload>;
|
|
48
|
-
export declare function
|
|
59
|
+
export declare function performSlackUploads({ uploads, channelId, threadTs, initialComment, slack, token, fetchImpl, }: PerformSlackUploadsOptions): Promise<CompletedSlackUploads>;
|
|
60
|
+
export declare function performSlackUpload({ upload, channelId, threadTs, initialComment, slack, token, fetchImpl, }: PerformSlackUploadOptions): Promise<CompletedSlackUpload>;
|
|
49
61
|
export {};
|
package/dist/slack-upload.js
CHANGED
|
@@ -162,7 +162,7 @@ export async function prepareSlackUpload(params, cwd, tmpdir, fsDeps = {}) {
|
|
|
162
162
|
...(resolvedPath ? { resolvedPath } : {}),
|
|
163
163
|
};
|
|
164
164
|
}
|
|
165
|
-
|
|
165
|
+
async function reserveAndUploadSlackFile(upload, slack, token, fetchImpl) {
|
|
166
166
|
let getUploadResponse;
|
|
167
167
|
try {
|
|
168
168
|
getUploadResponse = await slack("files.getUploadURLExternal", token, buildUploadMetadataPayload(upload, true));
|
|
@@ -215,10 +215,37 @@ export async function performSlackUpload({ upload, channelId, threadTs, slack, t
|
|
|
215
215
|
const hint = isProxyOrFirewallFailure ? " [possible outbound proxy/firewall block]" : "";
|
|
216
216
|
throw new Error(`Slack raw upload failed (HTTP ${rawUploadResponse.status}${statusTextForHeader})${withDetails ? ` ${withDetails}` : ""}${hint}; host=${uploadHost}; filename=${upload.filename} byte_length=${upload.byteLength}`);
|
|
217
217
|
}
|
|
218
|
+
return { fileId, title: upload.title };
|
|
219
|
+
}
|
|
220
|
+
export async function performSlackUploads({ uploads, channelId, threadTs, initialComment, slack, token, fetchImpl = fetch, }) {
|
|
221
|
+
if (uploads.length === 0) {
|
|
222
|
+
throw new Error("At least one upload is required.");
|
|
223
|
+
}
|
|
224
|
+
const files = [];
|
|
225
|
+
for (const upload of uploads) {
|
|
226
|
+
files.push(await reserveAndUploadSlackFile(upload, slack, token, fetchImpl));
|
|
227
|
+
}
|
|
218
228
|
const response = await slack("files.completeUploadExternal", token, {
|
|
219
|
-
files:
|
|
229
|
+
files: files.map((file) => ({ id: file.fileId, title: file.title })),
|
|
220
230
|
channel_id: channelId,
|
|
221
231
|
...(threadTs ? { thread_ts: threadTs } : {}),
|
|
232
|
+
...(initialComment ? { initial_comment: initialComment } : {}),
|
|
222
233
|
});
|
|
234
|
+
return { fileIds: files.map((file) => file.fileId), response };
|
|
235
|
+
}
|
|
236
|
+
export async function performSlackUpload({ upload, channelId, threadTs, initialComment, slack, token, fetchImpl = fetch, }) {
|
|
237
|
+
const { fileIds, response } = await performSlackUploads({
|
|
238
|
+
uploads: [upload],
|
|
239
|
+
channelId,
|
|
240
|
+
...(threadTs ? { threadTs } : {}),
|
|
241
|
+
...(initialComment ? { initialComment } : {}),
|
|
242
|
+
slack,
|
|
243
|
+
token,
|
|
244
|
+
fetchImpl,
|
|
245
|
+
});
|
|
246
|
+
const fileId = fileIds[0];
|
|
247
|
+
if (!fileId) {
|
|
248
|
+
throw new Error("Slack upload did not return a file ID.");
|
|
249
|
+
}
|
|
223
250
|
return { fileId, response };
|
|
224
251
|
}
|