agent-slack 0.2.12 → 0.2.14
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 +15 -3
- package/dist/index.js +261 -81
- package/dist/index.js.map +8 -8
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Guiding principle:
|
|
|
9
9
|
|
|
10
10
|
## At a glance
|
|
11
11
|
|
|
12
|
-
- **Read**: fetch a message,
|
|
12
|
+
- **Read**: fetch a message, browse channel history, list full threads
|
|
13
13
|
- **Search**: messages + files (with filters)
|
|
14
14
|
- **Artifacts**: auto-download snippets/images/files to local paths for agents
|
|
15
15
|
- **Write**: reply in thread, add reactions
|
|
@@ -17,10 +17,18 @@ Guiding principle:
|
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
19
19
|
|
|
20
|
+
Recommended (Bun install script):
|
|
21
|
+
|
|
20
22
|
```bash
|
|
21
23
|
curl -fsSL https://raw.githubusercontent.com/stablyai/agent-slack/master/install.sh | sh
|
|
22
24
|
```
|
|
23
25
|
|
|
26
|
+
OR npm global install (requires Node >= 22.5):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm i -g agent-slack
|
|
30
|
+
```
|
|
31
|
+
|
|
24
32
|
## Agent skill
|
|
25
33
|
|
|
26
34
|
This repo ships an agent skill at `skills/agent-slack/` compatible with Claude Code, Codex, Cursor, etc
|
|
@@ -50,7 +58,7 @@ agent-slack
|
|
|
50
58
|
│ └── parse-curl
|
|
51
59
|
├── message
|
|
52
60
|
│ ├── get <target> # fetch 1 message (+ thread meta )
|
|
53
|
-
│ ├── list <target> # fetch
|
|
61
|
+
│ ├── list <target> # fetch thread or recent channel messages
|
|
54
62
|
│ ├── send <target> <text> # send / reply (does the right thing)
|
|
55
63
|
│ └── react
|
|
56
64
|
│ ├── add <target> <emoji>
|
|
@@ -138,6 +146,9 @@ agent-slack message get "https://workspace.slack.com/archives/C123/p170000000000
|
|
|
138
146
|
|
|
139
147
|
# Full thread for a message
|
|
140
148
|
agent-slack message list "https://workspace.slack.com/archives/C123/p1700000000000000"
|
|
149
|
+
|
|
150
|
+
# Recent channel messages (browse channel history)
|
|
151
|
+
agent-slack message list "#general" --limit 20
|
|
141
152
|
```
|
|
142
153
|
|
|
143
154
|
Optional:
|
|
@@ -158,7 +169,7 @@ agent-slack message get "https://workspace.slack.com/archives/C123/p170000000000
|
|
|
158
169
|
}
|
|
159
170
|
```
|
|
160
171
|
|
|
161
|
-
**`message list`** fetches all
|
|
172
|
+
**`message list`** fetches all replies in a thread, or recent channel messages when no thread is specified. Use this when you need the full conversation:
|
|
162
173
|
|
|
163
174
|
```json
|
|
164
175
|
{
|
|
@@ -173,6 +184,7 @@ When to use which:
|
|
|
173
184
|
|
|
174
185
|
- Use `get` to check a single message or see if there's a thread worth expanding
|
|
175
186
|
- Use `list` to read an entire thread conversation
|
|
187
|
+
- Use `list` on a channel (without `--thread-ts`) to browse recent channel messages
|
|
176
188
|
|
|
177
189
|
### Files (snippets/images/attachments)
|
|
178
190
|
|
package/dist/index.js
CHANGED
|
@@ -176,9 +176,9 @@ function parseSlackCurlCommand(curlInput) {
|
|
|
176
176
|
// src/auth/desktop.ts
|
|
177
177
|
import { cp, mkdir, rm, unlink } from "node:fs/promises";
|
|
178
178
|
import { existsSync as existsSync2 } from "node:fs";
|
|
179
|
-
import { execFileSync
|
|
179
|
+
import { execFileSync } from "node:child_process";
|
|
180
180
|
import { pbkdf2Sync, createDecipheriv } from "node:crypto";
|
|
181
|
-
import { homedir } from "node:os";
|
|
181
|
+
import { homedir, platform as platform2 } from "node:os";
|
|
182
182
|
import { join as join3 } from "node:path";
|
|
183
183
|
|
|
184
184
|
// src/lib/leveldb-reader.ts
|
|
@@ -441,6 +441,21 @@ async function findKeysContaining(dir, substring) {
|
|
|
441
441
|
}
|
|
442
442
|
|
|
443
443
|
// src/auth/desktop.ts
|
|
444
|
+
function isMissingBunSqliteModule(error) {
|
|
445
|
+
if (!error || typeof error !== "object") {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
const err = error;
|
|
449
|
+
const code = typeof err.code === "string" ? err.code : "";
|
|
450
|
+
const message = typeof err.message === "string" ? err.message : "";
|
|
451
|
+
if (code === "ERR_MODULE_NOT_FOUND" || code === "ERR_UNSUPPORTED_ESM_URL_SCHEME") {
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
if (!message.includes("bun:sqlite")) {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
return message.includes("Cannot find module") || message.includes("Unknown builtin module") || message.includes("unsupported URL scheme") || message.includes("Only URLs with a scheme in");
|
|
458
|
+
}
|
|
444
459
|
async function queryReadonlySqlite(dbPath, sql) {
|
|
445
460
|
try {
|
|
446
461
|
const { Database } = await import("bun:sqlite");
|
|
@@ -450,7 +465,10 @@ async function queryReadonlySqlite(dbPath, sql) {
|
|
|
450
465
|
} finally {
|
|
451
466
|
db.close();
|
|
452
467
|
}
|
|
453
|
-
} catch {
|
|
468
|
+
} catch (error) {
|
|
469
|
+
if (!isMissingBunSqliteModule(error)) {
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
454
472
|
const { DatabaseSync } = await import("node:sqlite");
|
|
455
473
|
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
456
474
|
try {
|
|
@@ -460,14 +478,24 @@ async function queryReadonlySqlite(dbPath, sql) {
|
|
|
460
478
|
}
|
|
461
479
|
}
|
|
462
480
|
}
|
|
481
|
+
var PLATFORM = platform2();
|
|
482
|
+
var IS_MACOS2 = PLATFORM === "darwin";
|
|
483
|
+
var IS_LINUX = PLATFORM === "linux";
|
|
463
484
|
var SLACK_SUPPORT_DIR_ELECTRON = join3(homedir(), "Library", "Application Support", "Slack");
|
|
464
485
|
var SLACK_SUPPORT_DIR_APPSTORE = join3(homedir(), "Library", "Containers", "com.tinyspeck.slackmacgap", "Data", "Library", "Application Support", "Slack");
|
|
486
|
+
var SLACK_SUPPORT_DIR_LINUX = join3(homedir(), ".config", "Slack");
|
|
487
|
+
var SLACK_SUPPORT_DIR_LINUX_FLATPAK = join3(homedir(), ".var", "app", "com.slack.Slack", "config", "Slack");
|
|
465
488
|
function getSlackPaths() {
|
|
466
|
-
const candidates = [SLACK_SUPPORT_DIR_ELECTRON, SLACK_SUPPORT_DIR_APPSTORE];
|
|
489
|
+
const candidates = IS_MACOS2 ? [SLACK_SUPPORT_DIR_ELECTRON, SLACK_SUPPORT_DIR_APPSTORE] : IS_LINUX ? [SLACK_SUPPORT_DIR_LINUX_FLATPAK, SLACK_SUPPORT_DIR_LINUX] : [];
|
|
490
|
+
if (candidates.length === 0) {
|
|
491
|
+
throw new Error(`Slack Desktop extraction is not supported on ${PLATFORM}.`);
|
|
492
|
+
}
|
|
467
493
|
for (const dir of candidates) {
|
|
468
494
|
const leveldbDir = join3(dir, "Local Storage", "leveldb");
|
|
469
495
|
if (existsSync2(leveldbDir)) {
|
|
470
|
-
|
|
496
|
+
const cookiesDbCandidates = [join3(dir, "Network", "Cookies"), join3(dir, "Cookies")];
|
|
497
|
+
const cookiesDb = cookiesDbCandidates.find((candidate) => existsSync2(candidate)) || cookiesDbCandidates[0];
|
|
498
|
+
return { leveldbDir, cookiesDb };
|
|
471
499
|
}
|
|
472
500
|
}
|
|
473
501
|
throw new Error(`Slack Desktop data not found. Checked:
|
|
@@ -493,11 +521,17 @@ async function snapshotLevelDb(srcDir) {
|
|
|
493
521
|
const base = join3(homedir(), ".config", "agent-slack", "cache", "leveldb-snapshots");
|
|
494
522
|
const dest = join3(base, `${Date.now()}`);
|
|
495
523
|
await mkdir(base, { recursive: true });
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
524
|
+
let useNodeCopy = !IS_MACOS2;
|
|
525
|
+
if (IS_MACOS2) {
|
|
526
|
+
try {
|
|
527
|
+
execFileSync("cp", ["-cR", srcDir, dest], {
|
|
528
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
529
|
+
});
|
|
530
|
+
} catch {
|
|
531
|
+
useNodeCopy = true;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (useNodeCopy) {
|
|
501
535
|
await cp(srcDir, dest, { recursive: true, force: true });
|
|
502
536
|
}
|
|
503
537
|
try {
|
|
@@ -552,11 +586,18 @@ async function extractTeamsFromSlackLevelDb(leveldbDir) {
|
|
|
552
586
|
const localConfigV3 = Buffer.from("localConfig_v3");
|
|
553
587
|
const entries = await findKeysContaining(snap, Buffer.from("localConfig_v"));
|
|
554
588
|
let configBuf = null;
|
|
589
|
+
let configRank = -1n;
|
|
555
590
|
for (const entry of entries) {
|
|
556
591
|
if (entry.key.includes(localConfigV2) || entry.key.includes(localConfigV3)) {
|
|
557
592
|
if (entry.value && entry.value.length > 0) {
|
|
558
|
-
|
|
559
|
-
|
|
593
|
+
let rank = 0n;
|
|
594
|
+
if (entry.key.length >= 8) {
|
|
595
|
+
rank = entry.key.readBigUInt64LE(entry.key.length - 8);
|
|
596
|
+
}
|
|
597
|
+
if (!configBuf || rank >= configRank) {
|
|
598
|
+
configBuf = entry.value;
|
|
599
|
+
configRank = rank;
|
|
600
|
+
}
|
|
560
601
|
}
|
|
561
602
|
}
|
|
562
603
|
}
|
|
@@ -577,30 +618,59 @@ async function extractTeamsFromSlackLevelDb(leveldbDir) {
|
|
|
577
618
|
} catch {}
|
|
578
619
|
}
|
|
579
620
|
}
|
|
580
|
-
function
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
621
|
+
function getSafeStoragePasswords(prefix) {
|
|
622
|
+
if (IS_MACOS2) {
|
|
623
|
+
const services = ["Slack Safe Storage", "Chrome Safe Storage", "Chromium Safe Storage"];
|
|
624
|
+
const passwords = [];
|
|
625
|
+
for (const service of services) {
|
|
626
|
+
try {
|
|
627
|
+
const out = execFileSync("security", ["find-generic-password", "-w", "-s", service], {
|
|
628
|
+
encoding: "utf8",
|
|
629
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
630
|
+
}).trim();
|
|
631
|
+
if (out) {
|
|
632
|
+
passwords.push(out);
|
|
633
|
+
}
|
|
634
|
+
} catch {}
|
|
635
|
+
}
|
|
636
|
+
if (passwords.length > 0) {
|
|
637
|
+
return passwords;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (IS_LINUX) {
|
|
641
|
+
const attributes = [
|
|
642
|
+
["application", "com.slack.Slack"],
|
|
643
|
+
["application", "Slack"],
|
|
644
|
+
["application", "slack"],
|
|
645
|
+
["service", "Slack Safe Storage"]
|
|
646
|
+
];
|
|
647
|
+
const passwords = [];
|
|
648
|
+
for (const pair of attributes) {
|
|
649
|
+
try {
|
|
650
|
+
const out = execFileSync("secret-tool", ["lookup", ...pair], {
|
|
651
|
+
encoding: "utf8",
|
|
652
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
653
|
+
}).trim();
|
|
654
|
+
if (out) {
|
|
655
|
+
passwords.push(out);
|
|
656
|
+
}
|
|
657
|
+
} catch {}
|
|
658
|
+
}
|
|
659
|
+
if (prefix === "v11") {
|
|
660
|
+
passwords.push("");
|
|
661
|
+
}
|
|
662
|
+
passwords.push("peanuts");
|
|
663
|
+
return [...new Set(passwords)];
|
|
592
664
|
}
|
|
593
|
-
throw new Error(
|
|
665
|
+
throw new Error("Could not read Safe Storage password from desktop keychain.");
|
|
594
666
|
}
|
|
595
|
-
function decryptChromiumCookieValue(
|
|
596
|
-
if (!
|
|
667
|
+
function decryptChromiumCookieValue(data, password) {
|
|
668
|
+
if (!data || data.length === 0) {
|
|
597
669
|
return "";
|
|
598
670
|
}
|
|
599
|
-
const prefix = encrypted.subarray(0, 3).toString("utf8");
|
|
600
|
-
const data = prefix === "v10" || prefix === "v11" ? encrypted.subarray(3) : encrypted;
|
|
601
671
|
const salt = Buffer.from("saltysalt", "utf8");
|
|
602
672
|
const iv = Buffer.alloc(16, " ");
|
|
603
|
-
const key = pbkdf2Sync(password, salt, 1003, 16, "sha1");
|
|
673
|
+
const key = pbkdf2Sync(password, salt, IS_LINUX ? 1 : 1003, 16, "sha1");
|
|
604
674
|
const decipher = createDecipheriv("aes-128-cbc", key, iv);
|
|
605
675
|
decipher.setAutoPadding(true);
|
|
606
676
|
const plain = Buffer.concat([decipher.update(data), decipher.final()]);
|
|
@@ -640,13 +710,19 @@ async function extractCookieDFromSlackCookiesDb(cookiesPath) {
|
|
|
640
710
|
if (encrypted.length === 0) {
|
|
641
711
|
throw new Error("Slack 'd' cookie had no encrypted_value");
|
|
642
712
|
}
|
|
643
|
-
const
|
|
644
|
-
const
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
713
|
+
const prefix = encrypted.subarray(0, 3).toString("utf8");
|
|
714
|
+
const data = prefix === "v10" || prefix === "v11" ? encrypted.subarray(3) : encrypted;
|
|
715
|
+
const passwords = getSafeStoragePasswords(prefix);
|
|
716
|
+
for (const password of passwords) {
|
|
717
|
+
try {
|
|
718
|
+
const decrypted = decryptChromiumCookieValue(data, password);
|
|
719
|
+
const match = decrypted.match(/xoxd-[A-Za-z0-9%/+_=.-]+/);
|
|
720
|
+
if (match) {
|
|
721
|
+
return match[0];
|
|
722
|
+
}
|
|
723
|
+
} catch {}
|
|
648
724
|
}
|
|
649
|
-
|
|
725
|
+
throw new Error("Could not locate xoxd-* in decrypted Slack cookie");
|
|
650
726
|
}
|
|
651
727
|
async function extractFromSlackDesktop() {
|
|
652
728
|
const { leveldbDir, cookiesDb } = getSlackPaths();
|
|
@@ -720,11 +796,11 @@ var CredentialsSchema = z.object({
|
|
|
720
796
|
});
|
|
721
797
|
|
|
722
798
|
// src/auth/keychain.ts
|
|
723
|
-
import { platform as
|
|
799
|
+
import { platform as platform3 } from "node:os";
|
|
724
800
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
725
|
-
var
|
|
801
|
+
var IS_MACOS3 = platform3() === "darwin";
|
|
726
802
|
function keychainGet(account, service) {
|
|
727
|
-
if (!
|
|
803
|
+
if (!IS_MACOS3) {
|
|
728
804
|
return null;
|
|
729
805
|
}
|
|
730
806
|
try {
|
|
@@ -735,7 +811,7 @@ function keychainGet(account, service) {
|
|
|
735
811
|
}
|
|
736
812
|
}
|
|
737
813
|
function keychainSet(input) {
|
|
738
|
-
if (!
|
|
814
|
+
if (!IS_MACOS3) {
|
|
739
815
|
return false;
|
|
740
816
|
}
|
|
741
817
|
const { account, value, service } = input;
|
|
@@ -755,9 +831,9 @@ function keychainSet(input) {
|
|
|
755
831
|
}
|
|
756
832
|
|
|
757
833
|
// src/auth/store.ts
|
|
758
|
-
import { platform as
|
|
834
|
+
import { platform as platform4 } from "node:os";
|
|
759
835
|
var KEYCHAIN_PLACEHOLDER = "__KEYCHAIN__";
|
|
760
|
-
var
|
|
836
|
+
var IS_MACOS4 = platform4() === "darwin";
|
|
761
837
|
function normalizeWorkspaceUrl(workspaceUrl) {
|
|
762
838
|
const u = new URL(workspaceUrl);
|
|
763
839
|
return `${u.protocol}//${u.host}`;
|
|
@@ -811,7 +887,7 @@ async function saveCredentials(credentials) {
|
|
|
811
887
|
}))
|
|
812
888
|
};
|
|
813
889
|
const filePayload = structuredClone(payload);
|
|
814
|
-
if (
|
|
890
|
+
if (IS_MACOS4) {
|
|
815
891
|
const firstBrowser = payload.workspaces.find((w) => w.auth.auth_type === "browser");
|
|
816
892
|
let xoxdStored = false;
|
|
817
893
|
if (firstBrowser?.auth.auth_type === "browser" && !isPlaceholderSecret(firstBrowser.auth.xoxd_cookie)) {
|
|
@@ -941,8 +1017,24 @@ async function resolveChannelId(client, input) {
|
|
|
941
1017
|
if (!name) {
|
|
942
1018
|
throw new Error("Channel name is empty");
|
|
943
1019
|
}
|
|
1020
|
+
try {
|
|
1021
|
+
const searchResp = await client.api("search.messages", {
|
|
1022
|
+
query: `in:#${name}`,
|
|
1023
|
+
count: 1,
|
|
1024
|
+
sort: "timestamp",
|
|
1025
|
+
sort_dir: "desc"
|
|
1026
|
+
});
|
|
1027
|
+
const messages = isRecord4(searchResp) ? searchResp.messages : null;
|
|
1028
|
+
const matches = isRecord4(messages) ? asArray(messages.matches).filter(isRecord4) : [];
|
|
1029
|
+
if (matches.length > 0) {
|
|
1030
|
+
const channel = isRecord4(matches[0].channel) ? matches[0].channel : null;
|
|
1031
|
+
const channelId = channel ? getString(channel.id) : undefined;
|
|
1032
|
+
if (channelId) {
|
|
1033
|
+
return channelId;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
} catch {}
|
|
944
1037
|
let cursor;
|
|
945
|
-
const matches = [];
|
|
946
1038
|
for (;; ) {
|
|
947
1039
|
const resp = await client.api("conversations.list", {
|
|
948
1040
|
exclude_archived: true,
|
|
@@ -953,11 +1045,7 @@ async function resolveChannelId(client, input) {
|
|
|
953
1045
|
const chans = asArray(resp.channels).filter(isRecord4);
|
|
954
1046
|
for (const c of chans) {
|
|
955
1047
|
if (getString(c.name) === name && getString(c.id)) {
|
|
956
|
-
|
|
957
|
-
id: getString(c.id) ?? "",
|
|
958
|
-
name: getString(c.name) ?? undefined,
|
|
959
|
-
is_private: typeof c.is_private === "boolean" ? c.is_private : undefined
|
|
960
|
-
});
|
|
1048
|
+
return getString(c.id);
|
|
961
1049
|
}
|
|
962
1050
|
}
|
|
963
1051
|
const meta = isRecord4(resp.response_metadata) ? resp.response_metadata : null;
|
|
@@ -967,13 +1055,7 @@ async function resolveChannelId(client, input) {
|
|
|
967
1055
|
}
|
|
968
1056
|
cursor = next;
|
|
969
1057
|
}
|
|
970
|
-
|
|
971
|
-
return matches[0].id;
|
|
972
|
-
}
|
|
973
|
-
if (matches.length === 0) {
|
|
974
|
-
throw new Error(`Could not resolve channel name: #${name}`);
|
|
975
|
-
}
|
|
976
|
-
throw new Error(`Ambiguous channel name: #${name} (matched ${matches.length} channels: ${matches.map((m) => m.id).join(", ")})`);
|
|
1058
|
+
throw new Error(`Could not resolve channel name: #${name}`);
|
|
977
1059
|
}
|
|
978
1060
|
function isRecord4(value) {
|
|
979
1061
|
return typeof value === "object" && value !== null;
|
|
@@ -1099,7 +1181,7 @@ function normalizeUrl(u) {
|
|
|
1099
1181
|
return `${url.protocol}//${url.host}`;
|
|
1100
1182
|
}
|
|
1101
1183
|
async function refreshFromDesktopIfPossible() {
|
|
1102
|
-
if (process.platform !== "darwin") {
|
|
1184
|
+
if (process.platform !== "darwin" && process.platform !== "linux") {
|
|
1103
1185
|
return false;
|
|
1104
1186
|
}
|
|
1105
1187
|
try {
|
|
@@ -2123,6 +2205,43 @@ async function findMessageInThread(client, input) {
|
|
|
2123
2205
|
}
|
|
2124
2206
|
return;
|
|
2125
2207
|
}
|
|
2208
|
+
async function fetchChannelHistory(client, input) {
|
|
2209
|
+
const raw = input.limit ?? 25;
|
|
2210
|
+
const limit = Number.isFinite(raw) ? Math.min(Math.max(raw, 1), 200) : 25;
|
|
2211
|
+
const out = [];
|
|
2212
|
+
const resp = await client.api("conversations.history", {
|
|
2213
|
+
channel: input.channelId,
|
|
2214
|
+
limit,
|
|
2215
|
+
latest: input.latest,
|
|
2216
|
+
oldest: input.oldest,
|
|
2217
|
+
include_all_metadata: input.includeReactions ? true : undefined
|
|
2218
|
+
});
|
|
2219
|
+
const messages = asArray2(resp.messages);
|
|
2220
|
+
for (const m of messages) {
|
|
2221
|
+
if (!isRecord8(m)) {
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
const files = asArray2(m.files).map((f) => toSlackFileSummary(f)).filter((f) => f !== null);
|
|
2225
|
+
const enrichedFiles = files.length > 0 ? await enrichFiles(client, files) : undefined;
|
|
2226
|
+
const text = getString4(m.text) ?? "";
|
|
2227
|
+
out.push({
|
|
2228
|
+
channel_id: input.channelId,
|
|
2229
|
+
ts: getString4(m.ts) ?? "",
|
|
2230
|
+
thread_ts: getString4(m.thread_ts),
|
|
2231
|
+
reply_count: getNumber(m.reply_count),
|
|
2232
|
+
user: getString4(m.user),
|
|
2233
|
+
bot_id: getString4(m.bot_id),
|
|
2234
|
+
text,
|
|
2235
|
+
markdown: slackMrkdwnToMarkdown(text),
|
|
2236
|
+
blocks: Array.isArray(m.blocks) ? m.blocks : undefined,
|
|
2237
|
+
attachments: Array.isArray(m.attachments) ? m.attachments : undefined,
|
|
2238
|
+
files: enrichedFiles,
|
|
2239
|
+
reactions: Array.isArray(m.reactions) ? m.reactions : undefined
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
out.sort((a, b) => Number.parseFloat(a.ts) - Number.parseFloat(b.ts));
|
|
2243
|
+
return out;
|
|
2244
|
+
}
|
|
2126
2245
|
async function fetchThread(client, input) {
|
|
2127
2246
|
const out = [];
|
|
2128
2247
|
let cursor;
|
|
@@ -2286,6 +2405,8 @@ function parseMsgTarget(input) {
|
|
|
2286
2405
|
}
|
|
2287
2406
|
|
|
2288
2407
|
// src/cli/message-actions.ts
|
|
2408
|
+
import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
2409
|
+
import { join as join8 } from "node:path";
|
|
2289
2410
|
function isRecord9(value) {
|
|
2290
2411
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2291
2412
|
}
|
|
@@ -2295,6 +2416,16 @@ function asArray3(value) {
|
|
|
2295
2416
|
function getNumber2(value) {
|
|
2296
2417
|
return typeof value === "number" ? value : undefined;
|
|
2297
2418
|
}
|
|
2419
|
+
function parseLimit(raw) {
|
|
2420
|
+
if (raw === undefined) {
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
const n = Number.parseInt(raw, 10);
|
|
2424
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
2425
|
+
throw new Error(`Invalid --limit value "${raw}": must be a positive integer`);
|
|
2426
|
+
}
|
|
2427
|
+
return n;
|
|
2428
|
+
}
|
|
2298
2429
|
async function getThreadSummary(client, input) {
|
|
2299
2430
|
const replyCount = input.msg.reply_count ?? 0;
|
|
2300
2431
|
const rootTs = input.msg.thread_ts ?? (replyCount > 0 ? input.msg.ts : null);
|
|
@@ -2344,6 +2475,28 @@ function inferExt(file) {
|
|
|
2344
2475
|
const m = name.match(/\.([A-Za-z0-9]{1,10})$/);
|
|
2345
2476
|
return m ? m[1].toLowerCase() : null;
|
|
2346
2477
|
}
|
|
2478
|
+
var CANVAS_MODES = new Set(["canvas", "quip", "docs"]);
|
|
2479
|
+
function looksLikeAuthPage(html) {
|
|
2480
|
+
return /<form[^>]+signin|data-qa="signin|<title>[^<]*Sign\s*in/i.test(html);
|
|
2481
|
+
}
|
|
2482
|
+
async function downloadCanvasAsMarkdown(input) {
|
|
2483
|
+
const htmlPath = await downloadSlackFile({
|
|
2484
|
+
auth: input.auth,
|
|
2485
|
+
url: input.url,
|
|
2486
|
+
destDir: input.destDir,
|
|
2487
|
+
preferredName: `${input.fileId}.html`,
|
|
2488
|
+
options: { allowHtml: true }
|
|
2489
|
+
});
|
|
2490
|
+
const html = await readFile4(htmlPath, "utf8");
|
|
2491
|
+
if (looksLikeAuthPage(html)) {
|
|
2492
|
+
throw new Error("Downloaded auth/login page instead of canvas content (token may be expired)");
|
|
2493
|
+
}
|
|
2494
|
+
const md = htmlToMarkdown(html).trim();
|
|
2495
|
+
const safeName = `${input.fileId.replace(/[\\/<>:"|?*]/g, "_")}.md`;
|
|
2496
|
+
const mdPath = join8(input.destDir, safeName);
|
|
2497
|
+
await writeFile3(mdPath, md, "utf8");
|
|
2498
|
+
return mdPath;
|
|
2499
|
+
}
|
|
2347
2500
|
async function downloadFilesForMessages(input) {
|
|
2348
2501
|
const downloadedPaths = {};
|
|
2349
2502
|
const downloadsDir = await ensureDownloadsDir();
|
|
@@ -2352,18 +2505,31 @@ async function downloadFilesForMessages(input) {
|
|
|
2352
2505
|
if (downloadedPaths[f.id]) {
|
|
2353
2506
|
continue;
|
|
2354
2507
|
}
|
|
2355
|
-
const
|
|
2508
|
+
const isCanvas = f.mode != null && CANVAS_MODES.has(f.mode);
|
|
2509
|
+
const url = isCanvas ? f.url_private || f.url_private_download : f.url_private_download || f.url_private;
|
|
2356
2510
|
if (!url) {
|
|
2357
2511
|
continue;
|
|
2358
2512
|
}
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2513
|
+
try {
|
|
2514
|
+
if (isCanvas) {
|
|
2515
|
+
downloadedPaths[f.id] = await downloadCanvasAsMarkdown({
|
|
2516
|
+
auth: input.auth,
|
|
2517
|
+
fileId: f.id,
|
|
2518
|
+
url,
|
|
2519
|
+
destDir: downloadsDir
|
|
2520
|
+
});
|
|
2521
|
+
} else {
|
|
2522
|
+
const ext = inferExt(f);
|
|
2523
|
+
downloadedPaths[f.id] = await downloadSlackFile({
|
|
2524
|
+
auth: input.auth,
|
|
2525
|
+
url,
|
|
2526
|
+
destDir: downloadsDir,
|
|
2527
|
+
preferredName: `${f.id}${ext ? `.${ext}` : ""}`
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
} catch (err) {
|
|
2531
|
+
console.error(`Warning: skipping file ${f.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2532
|
+
}
|
|
2367
2533
|
}
|
|
2368
2534
|
}
|
|
2369
2535
|
return downloadedPaths;
|
|
@@ -2456,7 +2622,21 @@ async function handleMessageList(input) {
|
|
|
2456
2622
|
const threadTs = input.options.threadTs?.trim();
|
|
2457
2623
|
const ts = input.options.ts?.trim();
|
|
2458
2624
|
if (!threadTs && !ts) {
|
|
2459
|
-
|
|
2625
|
+
const includeReactions2 = Boolean(input.options.includeReactions);
|
|
2626
|
+
const limit = parseLimit(input.options.limit);
|
|
2627
|
+
const channelMessages = await fetchChannelHistory(client, {
|
|
2628
|
+
channelId,
|
|
2629
|
+
limit,
|
|
2630
|
+
latest: input.options.latest?.trim(),
|
|
2631
|
+
oldest: input.options.oldest?.trim(),
|
|
2632
|
+
includeReactions: includeReactions2
|
|
2633
|
+
});
|
|
2634
|
+
const downloadedPaths2 = await downloadFilesForMessages({ auth, messages: channelMessages });
|
|
2635
|
+
const maxBodyChars2 = Number.parseInt(input.options.maxBodyChars, 10);
|
|
2636
|
+
return pruneEmpty({
|
|
2637
|
+
channel_id: channelId,
|
|
2638
|
+
messages: channelMessages.map((m) => toCompactMessage(m, { maxBodyChars: maxBodyChars2, includeReactions: includeReactions2, downloadedPaths: downloadedPaths2 }))
|
|
2639
|
+
});
|
|
2460
2640
|
}
|
|
2461
2641
|
const rootTs = threadTs ?? await (async () => {
|
|
2462
2642
|
const ref = {
|
|
@@ -2574,7 +2754,7 @@ function registerMessageCommand(input) {
|
|
|
2574
2754
|
process.exitCode = 1;
|
|
2575
2755
|
}
|
|
2576
2756
|
});
|
|
2577
|
-
messageCmd.command("list").description("
|
|
2757
|
+
messageCmd.command("list").description("List recent channel messages, or fetch a full thread").argument("<target>", "Slack message URL, #channel, or channel ID").option("--workspace <url>", "Workspace URL (needed when using #channel/channel id and you have multiple workspaces)").option("--thread-ts <ts>", "Thread root ts (lists thread replies instead of channel history)").option("--ts <ts>", "Message ts (resolve message to its thread)").option("--limit <n>", "Max messages to return for channel history (default 25, max 200)").option("--oldest <ts>", "Only messages after this ts (channel history mode)").option("--latest <ts>", "Only messages before this ts (channel history mode)").option("--max-body-chars <n>", "Max content characters to include (default 8000, -1 for unlimited)", "8000").option("--include-reactions", "Include reactions + reacting users").action(async (...args) => {
|
|
2578
2758
|
const [targetInput, options] = args;
|
|
2579
2759
|
try {
|
|
2580
2760
|
const payload = await handleMessageList({ ctx: input.ctx, targetInput, options });
|
|
@@ -3359,13 +3539,13 @@ function registerSearchCommand(input) {
|
|
|
3359
3539
|
|
|
3360
3540
|
// src/lib/update.ts
|
|
3361
3541
|
import { createHash } from "node:crypto";
|
|
3362
|
-
import { chmod, copyFile, mkdir as mkdir5, readFile as
|
|
3542
|
+
import { chmod, copyFile, mkdir as mkdir5, readFile as readFile5, rename, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
|
|
3363
3543
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
3364
|
-
import { join as
|
|
3544
|
+
import { join as join9 } from "node:path";
|
|
3365
3545
|
var REPO = "stablyai/agent-slack";
|
|
3366
3546
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
3367
3547
|
function getCachePath() {
|
|
3368
|
-
return
|
|
3548
|
+
return join9(getAppDir(), "update-check.json");
|
|
3369
3549
|
}
|
|
3370
3550
|
function compareSemver(a, b) {
|
|
3371
3551
|
const pa = a.replace(/^v/, "").split(".").map(Number);
|
|
@@ -3422,24 +3602,24 @@ async function checkForUpdate(force = false) {
|
|
|
3422
3602
|
};
|
|
3423
3603
|
}
|
|
3424
3604
|
function detectPlatformAsset() {
|
|
3425
|
-
const
|
|
3605
|
+
const platform5 = process.platform === "win32" ? "windows" : process.platform;
|
|
3426
3606
|
const archMap = { x64: "x64", arm64: "arm64" };
|
|
3427
3607
|
const arch = archMap[process.arch] ?? process.arch;
|
|
3428
|
-
const ext =
|
|
3429
|
-
return `agent-slack-${
|
|
3608
|
+
const ext = platform5 === "windows" ? ".exe" : "";
|
|
3609
|
+
return `agent-slack-${platform5}-${arch}${ext}`;
|
|
3430
3610
|
}
|
|
3431
3611
|
async function sha256(filePath) {
|
|
3432
|
-
const data = await
|
|
3612
|
+
const data = await readFile5(filePath);
|
|
3433
3613
|
return createHash("sha256").update(data).digest("hex");
|
|
3434
3614
|
}
|
|
3435
3615
|
async function performUpdate(latest) {
|
|
3436
3616
|
const asset = detectPlatformAsset();
|
|
3437
3617
|
const tag = `v${latest}`;
|
|
3438
3618
|
const baseUrl = `https://github.com/${REPO}/releases/download/${tag}`;
|
|
3439
|
-
const tmp =
|
|
3619
|
+
const tmp = join9(tmpdir2(), `agent-slack-update-${Date.now()}`);
|
|
3440
3620
|
await mkdir5(tmp, { recursive: true });
|
|
3441
|
-
const binTmp =
|
|
3442
|
-
const sumsTmp =
|
|
3621
|
+
const binTmp = join9(tmp, asset);
|
|
3622
|
+
const sumsTmp = join9(tmp, "checksums-sha256.txt");
|
|
3443
3623
|
try {
|
|
3444
3624
|
const [binResp, sumsResp] = await Promise.all([
|
|
3445
3625
|
fetch(`${baseUrl}/${asset}`, { signal: AbortSignal.timeout(120000) }),
|
|
@@ -3451,9 +3631,9 @@ async function performUpdate(latest) {
|
|
|
3451
3631
|
if (!sumsResp.ok) {
|
|
3452
3632
|
return { success: false, message: `Failed to download checksums: HTTP ${sumsResp.status}` };
|
|
3453
3633
|
}
|
|
3454
|
-
await
|
|
3634
|
+
await writeFile4(binTmp, Buffer.from(await binResp.arrayBuffer()));
|
|
3455
3635
|
const sumsText = await sumsResp.text();
|
|
3456
|
-
await
|
|
3636
|
+
await writeFile4(sumsTmp, sumsText);
|
|
3457
3637
|
const expected = sumsText.split(`
|
|
3458
3638
|
`).map((line) => line.trim().split(/\s+/)).find((parts) => parts[1] === asset)?.[0];
|
|
3459
3639
|
if (!expected) {
|
|
@@ -3702,5 +3882,5 @@ if (subcommand && subcommand !== "update") {
|
|
|
3702
3882
|
backgroundUpdateCheck();
|
|
3703
3883
|
}
|
|
3704
3884
|
|
|
3705
|
-
//# debugId=
|
|
3885
|
+
//# debugId=596521D1CCB5DA7164756E2164756E21
|
|
3706
3886
|
//# sourceMappingURL=index.js.map
|