agent-slack 0.2.13 → 0.3.0
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 +47 -15
- package/dist/index.js +1217 -476
- package/dist/index.js.map +26 -20
- package/package.json +9 -9
package/dist/index.js
CHANGED
|
@@ -47,9 +47,12 @@ function getUserAgent() {
|
|
|
47
47
|
return `agent-slack/${getPackageVersion()}`;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
// src/auth/
|
|
51
|
-
import { execSync } from "node:child_process";
|
|
52
|
-
import {
|
|
50
|
+
// src/auth/brave.ts
|
|
51
|
+
import { execSync, execFileSync } from "node:child_process";
|
|
52
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
53
|
+
import { pbkdf2Sync, createDecipheriv } from "node:crypto";
|
|
54
|
+
import { homedir, platform } from "node:os";
|
|
55
|
+
import { join as join2 } from "node:path";
|
|
53
56
|
var IS_MACOS = platform() === "darwin";
|
|
54
57
|
function escapeOsaScript(script) {
|
|
55
58
|
return script.replace(/'/g, `'"'"'`);
|
|
@@ -61,6 +64,205 @@ function osascript(script) {
|
|
|
61
64
|
stdio: ["ignore", "pipe", "pipe"]
|
|
62
65
|
}).trim();
|
|
63
66
|
}
|
|
67
|
+
var TEAM_JSON_PATHS = [
|
|
68
|
+
"JSON.stringify(JSON.parse(localStorage.localConfig_v2).teams)",
|
|
69
|
+
"JSON.stringify(JSON.parse(localStorage.localConfig_v3).teams)",
|
|
70
|
+
"JSON.stringify(JSON.parse(localStorage.getItem('reduxPersist:localConfig'))?.teams || {})",
|
|
71
|
+
"JSON.stringify(window.boot_data?.teams || {})"
|
|
72
|
+
];
|
|
73
|
+
function teamsScript() {
|
|
74
|
+
const tryPaths = TEAM_JSON_PATHS.map((expr) => `try { var v = ${expr}; if (v && v !== '{}' && v !== 'null') return v; } catch(e) {}`);
|
|
75
|
+
return `
|
|
76
|
+
tell application "Brave Browser"
|
|
77
|
+
repeat with w in windows
|
|
78
|
+
repeat with t in tabs of w
|
|
79
|
+
if URL of t contains "slack.com" then
|
|
80
|
+
return execute t javascript "(function(){ ${tryPaths.join(" ")} return '{}'; })()"
|
|
81
|
+
end if
|
|
82
|
+
end repeat
|
|
83
|
+
end repeat
|
|
84
|
+
return "{}"
|
|
85
|
+
end tell
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
function isRecord(value) {
|
|
89
|
+
return typeof value === "object" && value !== null;
|
|
90
|
+
}
|
|
91
|
+
function toBraveTeam(value) {
|
|
92
|
+
if (!isRecord(value)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const token = typeof value.token === "string" ? value.token : null;
|
|
96
|
+
const url = typeof value.url === "string" ? value.url : null;
|
|
97
|
+
if (!token || !url || !token.startsWith("xoxc-")) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const name = typeof value.name === "string" ? value.name : undefined;
|
|
101
|
+
return { url, name, token };
|
|
102
|
+
}
|
|
103
|
+
function extractTeamsFromBraveTab() {
|
|
104
|
+
const teamsRaw = osascript(teamsScript());
|
|
105
|
+
let teamsObj = {};
|
|
106
|
+
try {
|
|
107
|
+
teamsObj = JSON.parse(teamsRaw || "{}");
|
|
108
|
+
} catch {
|
|
109
|
+
teamsObj = {};
|
|
110
|
+
}
|
|
111
|
+
const teamsRecord = isRecord(teamsObj) ? teamsObj : {};
|
|
112
|
+
return Object.values(teamsRecord).map((t) => toBraveTeam(t)).filter((t) => t !== null);
|
|
113
|
+
}
|
|
114
|
+
var BRAVE_COOKIES_DB = join2(homedir(), "Library", "Application Support", "BraveSoftware", "Brave-Browser", "Default", "Cookies");
|
|
115
|
+
function isMissingBunSqliteModule(error) {
|
|
116
|
+
if (!error || typeof error !== "object") {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
const err = error;
|
|
120
|
+
const code = typeof err.code === "string" ? err.code : "";
|
|
121
|
+
const message = typeof err.message === "string" ? err.message : "";
|
|
122
|
+
if (code === "ERR_MODULE_NOT_FOUND" || code === "ERR_UNSUPPORTED_ESM_URL_SCHEME") {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (!message.includes("bun:sqlite")) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return message.includes("Cannot find module") || message.includes("Unknown builtin module") || message.includes("unsupported URL scheme") || message.includes("Only URLs with a scheme in");
|
|
129
|
+
}
|
|
130
|
+
async function queryReadonlySqlite(dbPath, sql) {
|
|
131
|
+
try {
|
|
132
|
+
const { Database } = await import("bun:sqlite");
|
|
133
|
+
const db = new Database(dbPath, { readonly: true });
|
|
134
|
+
try {
|
|
135
|
+
return db.query(sql).all();
|
|
136
|
+
} finally {
|
|
137
|
+
db.close();
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (!isMissingBunSqliteModule(error)) {
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
144
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
145
|
+
try {
|
|
146
|
+
return db.prepare(sql).all();
|
|
147
|
+
} finally {
|
|
148
|
+
db.close();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function getSafeStoragePasswords() {
|
|
153
|
+
const services = [
|
|
154
|
+
"Brave Safe Storage",
|
|
155
|
+
"Brave Browser Safe Storage",
|
|
156
|
+
"Chrome Safe Storage",
|
|
157
|
+
"Chromium Safe Storage"
|
|
158
|
+
];
|
|
159
|
+
const passwords = [];
|
|
160
|
+
for (const service of services) {
|
|
161
|
+
try {
|
|
162
|
+
const out = execFileSync("security", ["find-generic-password", "-w", "-s", service], {
|
|
163
|
+
encoding: "utf8",
|
|
164
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
165
|
+
}).trim();
|
|
166
|
+
if (out) {
|
|
167
|
+
passwords.push(out);
|
|
168
|
+
}
|
|
169
|
+
} catch {}
|
|
170
|
+
}
|
|
171
|
+
return passwords;
|
|
172
|
+
}
|
|
173
|
+
function decryptChromiumCookieValue(data, password) {
|
|
174
|
+
if (!data || data.length === 0) {
|
|
175
|
+
return "";
|
|
176
|
+
}
|
|
177
|
+
const salt = Buffer.from("saltysalt", "utf8");
|
|
178
|
+
const iv = Buffer.alloc(16, " ");
|
|
179
|
+
const key = pbkdf2Sync(password, salt, 1003, 16, "sha1");
|
|
180
|
+
const decipher = createDecipheriv("aes-128-cbc", key, iv);
|
|
181
|
+
decipher.setAutoPadding(true);
|
|
182
|
+
const plain = Buffer.concat([decipher.update(data), decipher.final()]);
|
|
183
|
+
const marker = Buffer.from("xoxd-");
|
|
184
|
+
const idx = plain.indexOf(marker);
|
|
185
|
+
if (idx === -1) {
|
|
186
|
+
return plain.toString("utf8");
|
|
187
|
+
}
|
|
188
|
+
let end = idx;
|
|
189
|
+
while (end < plain.length) {
|
|
190
|
+
const b = plain[end];
|
|
191
|
+
if (b < 33 || b > 126) {
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
end++;
|
|
195
|
+
}
|
|
196
|
+
const rawToken = plain.subarray(idx, end).toString("utf8");
|
|
197
|
+
try {
|
|
198
|
+
return decodeURIComponent(rawToken);
|
|
199
|
+
} catch {
|
|
200
|
+
return rawToken;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function extractCookieDFromBrave() {
|
|
204
|
+
if (!existsSync2(BRAVE_COOKIES_DB)) {
|
|
205
|
+
throw new Error(`Brave Cookies DB not found: ${BRAVE_COOKIES_DB}`);
|
|
206
|
+
}
|
|
207
|
+
const rows = await queryReadonlySqlite(BRAVE_COOKIES_DB, "select host_key, name, value, encrypted_value from cookies where name = 'd' and host_key like '%slack.com' order by length(encrypted_value) desc");
|
|
208
|
+
if (!rows || rows.length === 0) {
|
|
209
|
+
throw new Error("No Slack 'd' cookie found in Brave");
|
|
210
|
+
}
|
|
211
|
+
const row = rows[0];
|
|
212
|
+
if (row.value && row.value.startsWith("xoxd-")) {
|
|
213
|
+
return row.value;
|
|
214
|
+
}
|
|
215
|
+
const encrypted = Buffer.from(row.encrypted_value || []);
|
|
216
|
+
if (encrypted.length === 0) {
|
|
217
|
+
throw new Error("Brave Slack 'd' cookie had no encrypted_value");
|
|
218
|
+
}
|
|
219
|
+
const prefix = encrypted.subarray(0, 3).toString("utf8");
|
|
220
|
+
const data = prefix === "v10" || prefix === "v11" ? encrypted.subarray(3) : encrypted;
|
|
221
|
+
const passwords = getSafeStoragePasswords();
|
|
222
|
+
for (const password of passwords) {
|
|
223
|
+
try {
|
|
224
|
+
const decrypted = decryptChromiumCookieValue(data, password);
|
|
225
|
+
const match = decrypted.match(/xoxd-[A-Za-z0-9%/+_=.-]+/);
|
|
226
|
+
if (match) {
|
|
227
|
+
return match[0];
|
|
228
|
+
}
|
|
229
|
+
} catch {}
|
|
230
|
+
}
|
|
231
|
+
throw new Error("Could not decrypt Slack 'd' cookie from Brave");
|
|
232
|
+
}
|
|
233
|
+
async function extractFromBrave() {
|
|
234
|
+
if (!IS_MACOS) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const teams = extractTeamsFromBraveTab();
|
|
239
|
+
if (teams.length === 0) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const cookie_d = await extractCookieDFromBrave();
|
|
243
|
+
if (!cookie_d || !cookie_d.startsWith("xoxd-")) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
return { cookie_d, teams };
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/auth/chrome.ts
|
|
253
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
254
|
+
import { platform as platform2 } from "node:os";
|
|
255
|
+
var IS_MACOS2 = platform2() === "darwin";
|
|
256
|
+
function escapeOsaScript2(script) {
|
|
257
|
+
return script.replace(/'/g, `'"'"'`);
|
|
258
|
+
}
|
|
259
|
+
function osascript2(script) {
|
|
260
|
+
return execSync2(`osascript -e '${escapeOsaScript2(script)}'`, {
|
|
261
|
+
encoding: "utf8",
|
|
262
|
+
timeout: 7000,
|
|
263
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
264
|
+
}).trim();
|
|
265
|
+
}
|
|
64
266
|
function cookieScript() {
|
|
65
267
|
return `
|
|
66
268
|
tell application "Google Chrome"
|
|
@@ -75,17 +277,17 @@ function cookieScript() {
|
|
|
75
277
|
end tell
|
|
76
278
|
`;
|
|
77
279
|
}
|
|
78
|
-
var
|
|
280
|
+
var TEAM_JSON_PATHS2 = [
|
|
79
281
|
"JSON.stringify(JSON.parse(localStorage.localConfig_v2).teams)",
|
|
80
282
|
"JSON.stringify(JSON.parse(localStorage.localConfig_v3).teams)",
|
|
81
283
|
"JSON.stringify(JSON.parse(localStorage.getItem('reduxPersist:localConfig'))?.teams || {})",
|
|
82
284
|
"JSON.stringify(window.boot_data?.teams || {})"
|
|
83
285
|
];
|
|
84
|
-
function
|
|
286
|
+
function isRecord2(value) {
|
|
85
287
|
return typeof value === "object" && value !== null;
|
|
86
288
|
}
|
|
87
289
|
function toChromeTeam(value) {
|
|
88
|
-
if (!
|
|
290
|
+
if (!isRecord2(value)) {
|
|
89
291
|
return null;
|
|
90
292
|
}
|
|
91
293
|
const token = typeof value.token === "string" ? value.token : null;
|
|
@@ -96,8 +298,8 @@ function toChromeTeam(value) {
|
|
|
96
298
|
const name = typeof value.name === "string" ? value.name : undefined;
|
|
97
299
|
return { url, name, token };
|
|
98
300
|
}
|
|
99
|
-
function
|
|
100
|
-
const tryPaths =
|
|
301
|
+
function teamsScript2() {
|
|
302
|
+
const tryPaths = TEAM_JSON_PATHS2.map((expr) => `try { var v = ${expr}; if (v && v !== '{}' && v !== 'null') return v; } catch(e) {}`);
|
|
101
303
|
return `
|
|
102
304
|
tell application "Google Chrome"
|
|
103
305
|
repeat with w in windows
|
|
@@ -112,22 +314,22 @@ function teamsScript() {
|
|
|
112
314
|
`;
|
|
113
315
|
}
|
|
114
316
|
function extractFromChrome() {
|
|
115
|
-
if (!
|
|
317
|
+
if (!IS_MACOS2) {
|
|
116
318
|
return null;
|
|
117
319
|
}
|
|
118
320
|
try {
|
|
119
|
-
const cookie =
|
|
321
|
+
const cookie = osascript2(cookieScript());
|
|
120
322
|
if (!cookie || !cookie.startsWith("xoxd-")) {
|
|
121
323
|
return null;
|
|
122
324
|
}
|
|
123
|
-
const teamsRaw =
|
|
325
|
+
const teamsRaw = osascript2(teamsScript2());
|
|
124
326
|
let teamsObj = {};
|
|
125
327
|
try {
|
|
126
328
|
teamsObj = JSON.parse(teamsRaw || "{}");
|
|
127
329
|
} catch {
|
|
128
330
|
teamsObj = {};
|
|
129
331
|
}
|
|
130
|
-
const teamsRecord =
|
|
332
|
+
const teamsRecord = isRecord2(teamsObj) ? teamsObj : {};
|
|
131
333
|
const teams = Object.values(teamsRecord).map((t) => toChromeTeam(t)).filter((t) => t !== null);
|
|
132
334
|
if (teams.length === 0) {
|
|
133
335
|
return null;
|
|
@@ -175,15 +377,15 @@ function parseSlackCurlCommand(curlInput) {
|
|
|
175
377
|
|
|
176
378
|
// src/auth/desktop.ts
|
|
177
379
|
import { cp, mkdir, rm, unlink } from "node:fs/promises";
|
|
178
|
-
import { existsSync as
|
|
179
|
-
import { execFileSync } from "node:child_process";
|
|
180
|
-
import { pbkdf2Sync, createDecipheriv } from "node:crypto";
|
|
181
|
-
import { homedir, platform as
|
|
182
|
-
import { join as
|
|
380
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
381
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
382
|
+
import { pbkdf2Sync as pbkdf2Sync2, createDecipheriv as createDecipheriv2 } from "node:crypto";
|
|
383
|
+
import { homedir as homedir2, platform as platform3 } from "node:os";
|
|
384
|
+
import { join as join4 } from "node:path";
|
|
183
385
|
|
|
184
386
|
// src/lib/leveldb-reader.ts
|
|
185
387
|
import { readdir, readFile } from "node:fs/promises";
|
|
186
|
-
import { join as
|
|
388
|
+
import { join as join3 } from "node:path";
|
|
187
389
|
import { snappyUncompress } from "hysnappy";
|
|
188
390
|
var LEVELDB_MAGIC = Buffer.from([87, 251, 128, 139, 36, 117, 71, 219]);
|
|
189
391
|
var COMPRESSION_NONE = 0;
|
|
@@ -425,12 +627,12 @@ async function readChromiumLevelDB(dir) {
|
|
|
425
627
|
}
|
|
426
628
|
const sstFiles = files.filter((f) => f.endsWith(".ldb") || f.endsWith(".sst"));
|
|
427
629
|
for (const file of sstFiles) {
|
|
428
|
-
const fileEntries = await parseSSTable(
|
|
630
|
+
const fileEntries = await parseSSTable(join3(dir, file));
|
|
429
631
|
entries.push(...fileEntries);
|
|
430
632
|
}
|
|
431
633
|
const logFiles = files.filter((f) => f.endsWith(".log"));
|
|
432
634
|
for (const file of logFiles) {
|
|
433
|
-
const fileEntries = await parseLogFile(
|
|
635
|
+
const fileEntries = await parseLogFile(join3(dir, file));
|
|
434
636
|
entries.push(...fileEntries);
|
|
435
637
|
}
|
|
436
638
|
return entries;
|
|
@@ -441,7 +643,7 @@ async function findKeysContaining(dir, substring) {
|
|
|
441
643
|
}
|
|
442
644
|
|
|
443
645
|
// src/auth/desktop.ts
|
|
444
|
-
function
|
|
646
|
+
function isMissingBunSqliteModule2(error) {
|
|
445
647
|
if (!error || typeof error !== "object") {
|
|
446
648
|
return false;
|
|
447
649
|
}
|
|
@@ -456,7 +658,7 @@ function isMissingBunSqliteModule(error) {
|
|
|
456
658
|
}
|
|
457
659
|
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
660
|
}
|
|
459
|
-
async function
|
|
661
|
+
async function queryReadonlySqlite2(dbPath, sql) {
|
|
460
662
|
try {
|
|
461
663
|
const { Database } = await import("bun:sqlite");
|
|
462
664
|
const db = new Database(dbPath, { readonly: true });
|
|
@@ -466,7 +668,7 @@ async function queryReadonlySqlite(dbPath, sql) {
|
|
|
466
668
|
db.close();
|
|
467
669
|
}
|
|
468
670
|
} catch (error) {
|
|
469
|
-
if (!
|
|
671
|
+
if (!isMissingBunSqliteModule2(error)) {
|
|
470
672
|
throw error;
|
|
471
673
|
}
|
|
472
674
|
const { DatabaseSync } = await import("node:sqlite");
|
|
@@ -478,35 +680,35 @@ async function queryReadonlySqlite(dbPath, sql) {
|
|
|
478
680
|
}
|
|
479
681
|
}
|
|
480
682
|
}
|
|
481
|
-
var PLATFORM =
|
|
482
|
-
var
|
|
683
|
+
var PLATFORM = platform3();
|
|
684
|
+
var IS_MACOS3 = PLATFORM === "darwin";
|
|
483
685
|
var IS_LINUX = PLATFORM === "linux";
|
|
484
|
-
var SLACK_SUPPORT_DIR_ELECTRON =
|
|
485
|
-
var SLACK_SUPPORT_DIR_APPSTORE =
|
|
486
|
-
var SLACK_SUPPORT_DIR_LINUX =
|
|
487
|
-
var SLACK_SUPPORT_DIR_LINUX_FLATPAK =
|
|
686
|
+
var SLACK_SUPPORT_DIR_ELECTRON = join4(homedir2(), "Library", "Application Support", "Slack");
|
|
687
|
+
var SLACK_SUPPORT_DIR_APPSTORE = join4(homedir2(), "Library", "Containers", "com.tinyspeck.slackmacgap", "Data", "Library", "Application Support", "Slack");
|
|
688
|
+
var SLACK_SUPPORT_DIR_LINUX = join4(homedir2(), ".config", "Slack");
|
|
689
|
+
var SLACK_SUPPORT_DIR_LINUX_FLATPAK = join4(homedir2(), ".var", "app", "com.slack.Slack", "config", "Slack");
|
|
488
690
|
function getSlackPaths() {
|
|
489
|
-
const candidates =
|
|
691
|
+
const candidates = IS_MACOS3 ? [SLACK_SUPPORT_DIR_ELECTRON, SLACK_SUPPORT_DIR_APPSTORE] : IS_LINUX ? [SLACK_SUPPORT_DIR_LINUX_FLATPAK, SLACK_SUPPORT_DIR_LINUX] : [];
|
|
490
692
|
if (candidates.length === 0) {
|
|
491
693
|
throw new Error(`Slack Desktop extraction is not supported on ${PLATFORM}.`);
|
|
492
694
|
}
|
|
493
695
|
for (const dir of candidates) {
|
|
494
|
-
const leveldbDir =
|
|
495
|
-
if (
|
|
496
|
-
const cookiesDbCandidates = [
|
|
497
|
-
const cookiesDb = cookiesDbCandidates.find((candidate) =>
|
|
696
|
+
const leveldbDir = join4(dir, "Local Storage", "leveldb");
|
|
697
|
+
if (existsSync3(leveldbDir)) {
|
|
698
|
+
const cookiesDbCandidates = [join4(dir, "Network", "Cookies"), join4(dir, "Cookies")];
|
|
699
|
+
const cookiesDb = cookiesDbCandidates.find((candidate) => existsSync3(candidate)) || cookiesDbCandidates[0];
|
|
498
700
|
return { leveldbDir, cookiesDb };
|
|
499
701
|
}
|
|
500
702
|
}
|
|
501
703
|
throw new Error(`Slack Desktop data not found. Checked:
|
|
502
|
-
- ${candidates.map((d) =>
|
|
704
|
+
- ${candidates.map((d) => join4(d, "Local Storage", "leveldb")).join(`
|
|
503
705
|
- `)}`);
|
|
504
706
|
}
|
|
505
|
-
function
|
|
707
|
+
function isRecord3(value) {
|
|
506
708
|
return typeof value === "object" && value !== null;
|
|
507
709
|
}
|
|
508
710
|
function toDesktopTeam(value) {
|
|
509
|
-
if (!
|
|
711
|
+
if (!isRecord3(value)) {
|
|
510
712
|
return null;
|
|
511
713
|
}
|
|
512
714
|
const url = typeof value.url === "string" ? value.url : null;
|
|
@@ -518,13 +720,13 @@ function toDesktopTeam(value) {
|
|
|
518
720
|
return { url, name, token };
|
|
519
721
|
}
|
|
520
722
|
async function snapshotLevelDb(srcDir) {
|
|
521
|
-
const base =
|
|
522
|
-
const dest =
|
|
723
|
+
const base = join4(homedir2(), ".config", "agent-slack", "cache", "leveldb-snapshots");
|
|
724
|
+
const dest = join4(base, `${Date.now()}`);
|
|
523
725
|
await mkdir(base, { recursive: true });
|
|
524
|
-
let useNodeCopy = !
|
|
525
|
-
if (
|
|
726
|
+
let useNodeCopy = !IS_MACOS3;
|
|
727
|
+
if (IS_MACOS3) {
|
|
526
728
|
try {
|
|
527
|
-
|
|
729
|
+
execFileSync2("cp", ["-cR", srcDir, dest], {
|
|
528
730
|
stdio: ["ignore", "ignore", "ignore"]
|
|
529
731
|
});
|
|
530
732
|
} catch {
|
|
@@ -535,7 +737,7 @@ async function snapshotLevelDb(srcDir) {
|
|
|
535
737
|
await cp(srcDir, dest, { recursive: true, force: true });
|
|
536
738
|
}
|
|
537
739
|
try {
|
|
538
|
-
await unlink(
|
|
740
|
+
await unlink(join4(dest, "LOCK"));
|
|
539
741
|
} catch {}
|
|
540
742
|
return dest;
|
|
541
743
|
}
|
|
@@ -577,7 +779,7 @@ function parseLocalConfig(raw) {
|
|
|
577
779
|
throw lastErr || new Error("localConfig not parseable");
|
|
578
780
|
}
|
|
579
781
|
async function extractTeamsFromSlackLevelDb(leveldbDir) {
|
|
580
|
-
if (!
|
|
782
|
+
if (!existsSync3(leveldbDir)) {
|
|
581
783
|
throw new Error(`Slack LevelDB not found: ${leveldbDir}`);
|
|
582
784
|
}
|
|
583
785
|
const snap = await snapshotLevelDb(leveldbDir);
|
|
@@ -605,8 +807,8 @@ async function extractTeamsFromSlackLevelDb(leveldbDir) {
|
|
|
605
807
|
throw new Error("Slack LevelDB did not contain localConfig_v2/v3");
|
|
606
808
|
}
|
|
607
809
|
const cfg = parseLocalConfig(configBuf);
|
|
608
|
-
const teamsValue =
|
|
609
|
-
const teamsObj =
|
|
810
|
+
const teamsValue = isRecord3(cfg) ? cfg.teams : undefined;
|
|
811
|
+
const teamsObj = isRecord3(teamsValue) ? teamsValue : {};
|
|
610
812
|
const teams = Object.values(teamsObj).map((t) => toDesktopTeam(t)).filter((t) => t !== null).filter((t) => t.token.startsWith("xoxc-"));
|
|
611
813
|
if (teams.length === 0) {
|
|
612
814
|
throw new Error("No xoxc tokens found in Slack localConfig");
|
|
@@ -618,13 +820,13 @@ async function extractTeamsFromSlackLevelDb(leveldbDir) {
|
|
|
618
820
|
} catch {}
|
|
619
821
|
}
|
|
620
822
|
}
|
|
621
|
-
function
|
|
622
|
-
if (
|
|
823
|
+
function getSafeStoragePasswords2(prefix) {
|
|
824
|
+
if (IS_MACOS3) {
|
|
623
825
|
const services = ["Slack Safe Storage", "Chrome Safe Storage", "Chromium Safe Storage"];
|
|
624
826
|
const passwords = [];
|
|
625
827
|
for (const service of services) {
|
|
626
828
|
try {
|
|
627
|
-
const out =
|
|
829
|
+
const out = execFileSync2("security", ["find-generic-password", "-w", "-s", service], {
|
|
628
830
|
encoding: "utf8",
|
|
629
831
|
stdio: ["ignore", "pipe", "ignore"]
|
|
630
832
|
}).trim();
|
|
@@ -647,7 +849,7 @@ function getSafeStoragePasswords(prefix) {
|
|
|
647
849
|
const passwords = [];
|
|
648
850
|
for (const pair of attributes) {
|
|
649
851
|
try {
|
|
650
|
-
const out =
|
|
852
|
+
const out = execFileSync2("secret-tool", ["lookup", ...pair], {
|
|
651
853
|
encoding: "utf8",
|
|
652
854
|
stdio: ["ignore", "pipe", "ignore"]
|
|
653
855
|
}).trim();
|
|
@@ -664,14 +866,14 @@ function getSafeStoragePasswords(prefix) {
|
|
|
664
866
|
}
|
|
665
867
|
throw new Error("Could not read Safe Storage password from desktop keychain.");
|
|
666
868
|
}
|
|
667
|
-
function
|
|
869
|
+
function decryptChromiumCookieValue2(data, password) {
|
|
668
870
|
if (!data || data.length === 0) {
|
|
669
871
|
return "";
|
|
670
872
|
}
|
|
671
873
|
const salt = Buffer.from("saltysalt", "utf8");
|
|
672
874
|
const iv = Buffer.alloc(16, " ");
|
|
673
|
-
const key =
|
|
674
|
-
const decipher =
|
|
875
|
+
const key = pbkdf2Sync2(password, salt, IS_LINUX ? 1 : 1003, 16, "sha1");
|
|
876
|
+
const decipher = createDecipheriv2("aes-128-cbc", key, iv);
|
|
675
877
|
decipher.setAutoPadding(true);
|
|
676
878
|
const plain = Buffer.concat([decipher.update(data), decipher.final()]);
|
|
677
879
|
const marker = Buffer.from("xoxd-");
|
|
@@ -695,10 +897,10 @@ function decryptChromiumCookieValue(data, password) {
|
|
|
695
897
|
}
|
|
696
898
|
}
|
|
697
899
|
async function extractCookieDFromSlackCookiesDb(cookiesPath) {
|
|
698
|
-
if (!
|
|
900
|
+
if (!existsSync3(cookiesPath)) {
|
|
699
901
|
throw new Error(`Slack Cookies DB not found: ${cookiesPath}`);
|
|
700
902
|
}
|
|
701
|
-
const rows = await
|
|
903
|
+
const rows = await queryReadonlySqlite2(cookiesPath, "select host_key, name, value, encrypted_value from cookies where name = 'd' and host_key like '%slack.com' order by length(encrypted_value) desc");
|
|
702
904
|
if (!rows || rows.length === 0) {
|
|
703
905
|
throw new Error("No Slack 'd' cookie found");
|
|
704
906
|
}
|
|
@@ -712,10 +914,10 @@ async function extractCookieDFromSlackCookiesDb(cookiesPath) {
|
|
|
712
914
|
}
|
|
713
915
|
const prefix = encrypted.subarray(0, 3).toString("utf8");
|
|
714
916
|
const data = prefix === "v10" || prefix === "v11" ? encrypted.subarray(3) : encrypted;
|
|
715
|
-
const passwords =
|
|
917
|
+
const passwords = getSafeStoragePasswords2(prefix);
|
|
716
918
|
for (const password of passwords) {
|
|
717
919
|
try {
|
|
718
|
-
const decrypted =
|
|
920
|
+
const decrypted = decryptChromiumCookieValue2(data, password);
|
|
719
921
|
const match = decrypted.match(/xoxd-[A-Za-z0-9%/+_=.-]+/);
|
|
720
922
|
if (match) {
|
|
721
923
|
return match[0];
|
|
@@ -736,10 +938,10 @@ async function extractFromSlackDesktop() {
|
|
|
736
938
|
}
|
|
737
939
|
|
|
738
940
|
// src/auth/paths.ts
|
|
739
|
-
import { homedir as
|
|
740
|
-
import { join as
|
|
741
|
-
var AGENT_SLACK_DIR =
|
|
742
|
-
var CREDENTIALS_FILE =
|
|
941
|
+
import { homedir as homedir3 } from "node:os";
|
|
942
|
+
import { join as join5 } from "node:path";
|
|
943
|
+
var AGENT_SLACK_DIR = join5(homedir3(), ".config", "agent-slack");
|
|
944
|
+
var CREDENTIALS_FILE = join5(AGENT_SLACK_DIR, "credentials.json");
|
|
743
945
|
var KEYCHAIN_SERVICE = "agent-slack";
|
|
744
946
|
|
|
745
947
|
// src/lib/fs.ts
|
|
@@ -750,7 +952,7 @@ async function readJsonFile(path) {
|
|
|
750
952
|
const raw = await readFile2(path, "utf8");
|
|
751
953
|
return JSON.parse(raw);
|
|
752
954
|
} catch (err) {
|
|
753
|
-
if (
|
|
955
|
+
if (isRecord4(err) && err.code === "ENOENT") {
|
|
754
956
|
return null;
|
|
755
957
|
}
|
|
756
958
|
if (err instanceof SyntaxError) {
|
|
@@ -764,7 +966,7 @@ async function writeJsonFile(path, data) {
|
|
|
764
966
|
await writeFile(path, `${JSON.stringify(data, null, 2)}
|
|
765
967
|
`, { mode: 384 });
|
|
766
968
|
}
|
|
767
|
-
function
|
|
969
|
+
function isRecord4(value) {
|
|
768
970
|
return typeof value === "object" && value !== null;
|
|
769
971
|
}
|
|
770
972
|
|
|
@@ -796,32 +998,32 @@ var CredentialsSchema = z.object({
|
|
|
796
998
|
});
|
|
797
999
|
|
|
798
1000
|
// src/auth/keychain.ts
|
|
799
|
-
import { platform as
|
|
800
|
-
import { execFileSync as
|
|
801
|
-
var
|
|
1001
|
+
import { platform as platform4 } from "node:os";
|
|
1002
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1003
|
+
var IS_MACOS4 = platform4() === "darwin";
|
|
802
1004
|
function keychainGet(account, service) {
|
|
803
|
-
if (!
|
|
1005
|
+
if (!IS_MACOS4) {
|
|
804
1006
|
return null;
|
|
805
1007
|
}
|
|
806
1008
|
try {
|
|
807
|
-
const result =
|
|
1009
|
+
const result = execFileSync3("security", ["find-generic-password", "-s", service, "-a", account, "-w"], { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] });
|
|
808
1010
|
return result.trim() || null;
|
|
809
1011
|
} catch {
|
|
810
1012
|
return null;
|
|
811
1013
|
}
|
|
812
1014
|
}
|
|
813
1015
|
function keychainSet(input) {
|
|
814
|
-
if (!
|
|
1016
|
+
if (!IS_MACOS4) {
|
|
815
1017
|
return false;
|
|
816
1018
|
}
|
|
817
1019
|
const { account, value, service } = input;
|
|
818
1020
|
try {
|
|
819
1021
|
try {
|
|
820
|
-
|
|
1022
|
+
execFileSync3("security", ["delete-generic-password", "-s", service, "-a", account], {
|
|
821
1023
|
stdio: ["pipe", "pipe", "ignore"]
|
|
822
1024
|
});
|
|
823
1025
|
} catch {}
|
|
824
|
-
|
|
1026
|
+
execFileSync3("security", ["add-generic-password", "-s", service, "-a", account, "-w", value], {
|
|
825
1027
|
stdio: "pipe"
|
|
826
1028
|
});
|
|
827
1029
|
return true;
|
|
@@ -831,9 +1033,9 @@ function keychainSet(input) {
|
|
|
831
1033
|
}
|
|
832
1034
|
|
|
833
1035
|
// src/auth/store.ts
|
|
834
|
-
import { platform as
|
|
1036
|
+
import { platform as platform5 } from "node:os";
|
|
835
1037
|
var KEYCHAIN_PLACEHOLDER = "__KEYCHAIN__";
|
|
836
|
-
var
|
|
1038
|
+
var IS_MACOS5 = platform5() === "darwin";
|
|
837
1039
|
function normalizeWorkspaceUrl(workspaceUrl) {
|
|
838
1040
|
const u = new URL(workspaceUrl);
|
|
839
1041
|
return `${u.protocol}//${u.host}`;
|
|
@@ -887,7 +1089,7 @@ async function saveCredentials(credentials) {
|
|
|
887
1089
|
}))
|
|
888
1090
|
};
|
|
889
1091
|
const filePayload = structuredClone(payload);
|
|
890
|
-
if (
|
|
1092
|
+
if (IS_MACOS5) {
|
|
891
1093
|
const firstBrowser = payload.workspaces.find((w) => w.auth.auth_type === "browser");
|
|
892
1094
|
let xoxdStored = false;
|
|
893
1095
|
if (firstBrowser?.auth.auth_type === "browser" && !isPlaceholderSecret(firstBrowser.auth.xoxd_cookie)) {
|
|
@@ -994,6 +1196,64 @@ async function resolveDefaultWorkspace() {
|
|
|
994
1196
|
return creds.workspaces[0] ?? null;
|
|
995
1197
|
}
|
|
996
1198
|
|
|
1199
|
+
// src/cli/workspace-selector.ts
|
|
1200
|
+
function normalizeUrl(u) {
|
|
1201
|
+
const url = new URL(u);
|
|
1202
|
+
return `${url.protocol}//${url.host}`;
|
|
1203
|
+
}
|
|
1204
|
+
function normalizedWorkspaceCandidates(workspace) {
|
|
1205
|
+
let host = "";
|
|
1206
|
+
try {
|
|
1207
|
+
host = new URL(workspace.workspace_url).host.toLowerCase();
|
|
1208
|
+
} catch {
|
|
1209
|
+
host = "";
|
|
1210
|
+
}
|
|
1211
|
+
const hostWithoutSlackSuffix = host.replace(/\.slack\.com$/i, "");
|
|
1212
|
+
return [
|
|
1213
|
+
workspace.workspace_url.toLowerCase(),
|
|
1214
|
+
host,
|
|
1215
|
+
hostWithoutSlackSuffix,
|
|
1216
|
+
workspace.workspace_name?.toLowerCase() ?? "",
|
|
1217
|
+
workspace.team_domain?.toLowerCase() ?? ""
|
|
1218
|
+
].filter(Boolean);
|
|
1219
|
+
}
|
|
1220
|
+
function resolveWorkspaceSelector(workspaces, selector) {
|
|
1221
|
+
const raw = selector.trim();
|
|
1222
|
+
if (!raw) {
|
|
1223
|
+
return { match: null, ambiguous: [] };
|
|
1224
|
+
}
|
|
1225
|
+
try {
|
|
1226
|
+
const normalized = normalizeUrl(raw).toLowerCase();
|
|
1227
|
+
const exact = workspaces.find((w) => w.workspace_url.toLowerCase() === normalized);
|
|
1228
|
+
if (exact) {
|
|
1229
|
+
return { match: exact, ambiguous: [] };
|
|
1230
|
+
}
|
|
1231
|
+
} catch {}
|
|
1232
|
+
const needle = raw.toLowerCase();
|
|
1233
|
+
const matches = workspaces.filter((workspace) => normalizedWorkspaceCandidates(workspace).some((candidate) => candidate.includes(needle)));
|
|
1234
|
+
if (matches.length === 1) {
|
|
1235
|
+
return { match: matches[0], ambiguous: [] };
|
|
1236
|
+
}
|
|
1237
|
+
if (matches.length > 1) {
|
|
1238
|
+
return { match: null, ambiguous: matches };
|
|
1239
|
+
}
|
|
1240
|
+
return { match: null, ambiguous: [] };
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// src/lib/object-type-guards.ts
|
|
1244
|
+
function isRecord5(value) {
|
|
1245
|
+
return typeof value === "object" && value !== null;
|
|
1246
|
+
}
|
|
1247
|
+
function asArray(value) {
|
|
1248
|
+
return Array.isArray(value) ? value : [];
|
|
1249
|
+
}
|
|
1250
|
+
function getString(value) {
|
|
1251
|
+
return typeof value === "string" ? value : undefined;
|
|
1252
|
+
}
|
|
1253
|
+
function getNumber(value) {
|
|
1254
|
+
return typeof value === "number" ? value : undefined;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
997
1257
|
// src/slack/channels.ts
|
|
998
1258
|
function isChannelId(input) {
|
|
999
1259
|
return /^[CDG][A-Z0-9]{8,}$/.test(input);
|
|
@@ -1017,8 +1277,24 @@ async function resolveChannelId(client, input) {
|
|
|
1017
1277
|
if (!name) {
|
|
1018
1278
|
throw new Error("Channel name is empty");
|
|
1019
1279
|
}
|
|
1280
|
+
try {
|
|
1281
|
+
const searchResp = await client.api("search.messages", {
|
|
1282
|
+
query: `in:#${name}`,
|
|
1283
|
+
count: 1,
|
|
1284
|
+
sort: "timestamp",
|
|
1285
|
+
sort_dir: "desc"
|
|
1286
|
+
});
|
|
1287
|
+
const messages = isRecord5(searchResp) ? searchResp.messages : null;
|
|
1288
|
+
const matches = isRecord5(messages) ? asArray(messages.matches).filter(isRecord5) : [];
|
|
1289
|
+
if (matches.length > 0) {
|
|
1290
|
+
const channel = isRecord5(matches[0].channel) ? matches[0].channel : null;
|
|
1291
|
+
const channelId = channel ? getString(channel.id) : undefined;
|
|
1292
|
+
if (channelId) {
|
|
1293
|
+
return channelId;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
} catch {}
|
|
1020
1297
|
let cursor;
|
|
1021
|
-
const matches = [];
|
|
1022
1298
|
for (;; ) {
|
|
1023
1299
|
const resp = await client.api("conversations.list", {
|
|
1024
1300
|
exclude_archived: true,
|
|
@@ -1026,39 +1302,20 @@ async function resolveChannelId(client, input) {
|
|
|
1026
1302
|
cursor,
|
|
1027
1303
|
types: "public_channel,private_channel"
|
|
1028
1304
|
});
|
|
1029
|
-
const chans = asArray(resp.channels).filter(
|
|
1305
|
+
const chans = asArray(resp.channels).filter(isRecord5);
|
|
1030
1306
|
for (const c of chans) {
|
|
1031
1307
|
if (getString(c.name) === name && getString(c.id)) {
|
|
1032
|
-
|
|
1033
|
-
id: getString(c.id) ?? "",
|
|
1034
|
-
name: getString(c.name) ?? undefined,
|
|
1035
|
-
is_private: typeof c.is_private === "boolean" ? c.is_private : undefined
|
|
1036
|
-
});
|
|
1308
|
+
return getString(c.id);
|
|
1037
1309
|
}
|
|
1038
1310
|
}
|
|
1039
|
-
const meta =
|
|
1311
|
+
const meta = isRecord5(resp.response_metadata) ? resp.response_metadata : null;
|
|
1040
1312
|
const next = meta ? getString(meta.next_cursor) : undefined;
|
|
1041
1313
|
if (!next) {
|
|
1042
1314
|
break;
|
|
1043
1315
|
}
|
|
1044
1316
|
cursor = next;
|
|
1045
1317
|
}
|
|
1046
|
-
|
|
1047
|
-
return matches[0].id;
|
|
1048
|
-
}
|
|
1049
|
-
if (matches.length === 0) {
|
|
1050
|
-
throw new Error(`Could not resolve channel name: #${name}`);
|
|
1051
|
-
}
|
|
1052
|
-
throw new Error(`Ambiguous channel name: #${name} (matched ${matches.length} channels: ${matches.map((m) => m.id).join(", ")})`);
|
|
1053
|
-
}
|
|
1054
|
-
function isRecord4(value) {
|
|
1055
|
-
return typeof value === "object" && value !== null;
|
|
1056
|
-
}
|
|
1057
|
-
function asArray(value) {
|
|
1058
|
-
return Array.isArray(value) ? value : [];
|
|
1059
|
-
}
|
|
1060
|
-
function getString(value) {
|
|
1061
|
-
return typeof value === "string" ? value : undefined;
|
|
1318
|
+
throw new Error(`Could not resolve channel name: #${name}`);
|
|
1062
1319
|
}
|
|
1063
1320
|
|
|
1064
1321
|
// src/slack/client.ts
|
|
@@ -1126,14 +1383,14 @@ class SlackApiClient {
|
|
|
1126
1383
|
if (!response.ok) {
|
|
1127
1384
|
throw new Error(`Slack HTTP ${response.status} calling ${input.method}`);
|
|
1128
1385
|
}
|
|
1129
|
-
if (!
|
|
1130
|
-
const error =
|
|
1386
|
+
if (!isRecord6(data) || data.ok !== true) {
|
|
1387
|
+
const error = isRecord6(data) && typeof data.error === "string" ? data.error : null;
|
|
1131
1388
|
throw new Error(error || `Slack API error calling ${input.method}`);
|
|
1132
1389
|
}
|
|
1133
1390
|
return data;
|
|
1134
1391
|
}
|
|
1135
1392
|
}
|
|
1136
|
-
function
|
|
1393
|
+
function isRecord6(value) {
|
|
1137
1394
|
return typeof value === "object" && value !== null;
|
|
1138
1395
|
}
|
|
1139
1396
|
|
|
@@ -1164,16 +1421,23 @@ async function assertWorkspaceSpecifiedForChannelNames(input) {
|
|
|
1164
1421
|
return;
|
|
1165
1422
|
}
|
|
1166
1423
|
if (!input.workspaceUrl) {
|
|
1167
|
-
throw new Error('Ambiguous channel name across multiple workspaces. Pass --workspace "
|
|
1424
|
+
throw new Error('Ambiguous channel name across multiple workspaces. Pass --workspace "<url-or-unique-substring>" (or set SLACK_WORKSPACE_URL).');
|
|
1168
1425
|
}
|
|
1169
1426
|
}
|
|
1170
1427
|
function isAuthErrorMessage(message) {
|
|
1171
1428
|
return /(?:^|[^a-z])(invalid_auth|token_expired)(?:$|[^a-z])/i.test(message);
|
|
1172
1429
|
}
|
|
1173
|
-
function
|
|
1430
|
+
function normalizeUrl2(u) {
|
|
1174
1431
|
const url = new URL(u);
|
|
1175
1432
|
return `${url.protocol}//${url.host}`;
|
|
1176
1433
|
}
|
|
1434
|
+
function tryNormalizeUrl(u) {
|
|
1435
|
+
try {
|
|
1436
|
+
return normalizeUrl2(u);
|
|
1437
|
+
} catch {
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1177
1441
|
async function refreshFromDesktopIfPossible() {
|
|
1178
1442
|
if (process.platform !== "darwin" && process.platform !== "linux") {
|
|
1179
1443
|
return false;
|
|
@@ -1181,7 +1445,7 @@ async function refreshFromDesktopIfPossible() {
|
|
|
1181
1445
|
try {
|
|
1182
1446
|
const extracted = await extractFromSlackDesktop();
|
|
1183
1447
|
await upsertWorkspaces(extracted.teams.map((team) => ({
|
|
1184
|
-
workspace_url:
|
|
1448
|
+
workspace_url: normalizeUrl2(team.url),
|
|
1185
1449
|
workspace_name: team.name,
|
|
1186
1450
|
auth: {
|
|
1187
1451
|
auth_type: "browser",
|
|
@@ -1227,18 +1491,35 @@ function pickAuthFromEnv() {
|
|
|
1227
1491
|
return { auth_type: "standard", token };
|
|
1228
1492
|
}
|
|
1229
1493
|
async function getClientForWorkspace(workspaceUrl) {
|
|
1494
|
+
const selector = workspaceUrl?.trim() || undefined;
|
|
1495
|
+
const normalizedSelectorUrl = selector ? tryNormalizeUrl(selector) : undefined;
|
|
1496
|
+
const selectorProvided = Boolean(selector);
|
|
1497
|
+
let resolvedWorkspaceUrl = normalizedSelectorUrl;
|
|
1498
|
+
if (selector) {
|
|
1499
|
+
const creds = await loadCredentials();
|
|
1500
|
+
const resolved = resolveWorkspaceSelector(creds.workspaces, selector);
|
|
1501
|
+
if (resolved.ambiguous.length > 0) {
|
|
1502
|
+
const options = resolved.ambiguous.map((w) => w.workspace_url).join(", ");
|
|
1503
|
+
throw new Error(`Workspace selector "${selector}" is ambiguous. Matches: ${options}. Pass a more specific selector or full workspace URL.`);
|
|
1504
|
+
}
|
|
1505
|
+
if (resolved.match) {
|
|
1506
|
+
resolvedWorkspaceUrl = resolved.match.workspace_url;
|
|
1507
|
+
} else if (!normalizedSelectorUrl) {
|
|
1508
|
+
resolvedWorkspaceUrl = undefined;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1230
1511
|
const env = pickAuthFromEnv();
|
|
1231
1512
|
if (env) {
|
|
1232
1513
|
const envWorkspaceUrl = process.env.SLACK_WORKSPACE_URL?.trim();
|
|
1233
|
-
const urlForBrowser =
|
|
1514
|
+
const urlForBrowser = resolvedWorkspaceUrl || envWorkspaceUrl;
|
|
1234
1515
|
return {
|
|
1235
1516
|
client: new SlackApiClient(env, { workspaceUrl: urlForBrowser }),
|
|
1236
1517
|
auth: env,
|
|
1237
1518
|
workspace_url: urlForBrowser
|
|
1238
1519
|
};
|
|
1239
1520
|
}
|
|
1240
|
-
if (
|
|
1241
|
-
const ws = await resolveWorkspaceForUrl(
|
|
1521
|
+
if (resolvedWorkspaceUrl) {
|
|
1522
|
+
const ws = await resolveWorkspaceForUrl(resolvedWorkspaceUrl);
|
|
1242
1523
|
if (ws) {
|
|
1243
1524
|
return {
|
|
1244
1525
|
client: new SlackApiClient(ws.auth, {
|
|
@@ -1249,20 +1530,22 @@ async function getClientForWorkspace(workspaceUrl) {
|
|
|
1249
1530
|
};
|
|
1250
1531
|
}
|
|
1251
1532
|
}
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1533
|
+
if (!selectorProvided) {
|
|
1534
|
+
const def = await resolveDefaultWorkspace();
|
|
1535
|
+
if (def) {
|
|
1536
|
+
return {
|
|
1537
|
+
client: new SlackApiClient(def.auth, {
|
|
1538
|
+
workspaceUrl: def.workspace_url
|
|
1539
|
+
}),
|
|
1540
|
+
auth: def.auth,
|
|
1541
|
+
workspace_url: def.workspace_url
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1261
1544
|
}
|
|
1262
1545
|
try {
|
|
1263
1546
|
const extracted = await extractFromSlackDesktop();
|
|
1264
1547
|
await upsertWorkspaces(extracted.teams.map((team) => ({
|
|
1265
|
-
workspace_url:
|
|
1548
|
+
workspace_url: normalizeUrl2(team.url),
|
|
1266
1549
|
workspace_name: team.name,
|
|
1267
1550
|
auth: {
|
|
1268
1551
|
auth_type: "browser",
|
|
@@ -1270,8 +1553,8 @@ async function getClientForWorkspace(workspaceUrl) {
|
|
|
1270
1553
|
xoxd_cookie: extracted.cookie_d
|
|
1271
1554
|
}
|
|
1272
1555
|
})));
|
|
1273
|
-
const desired =
|
|
1274
|
-
const chosen = desired ?? await resolveDefaultWorkspace();
|
|
1556
|
+
const desired = resolvedWorkspaceUrl ? await resolveWorkspaceForUrl(resolvedWorkspaceUrl) : selector ? resolveWorkspaceSelector((await loadCredentials()).workspaces, selector).match : await resolveDefaultWorkspace();
|
|
1557
|
+
const chosen = desired ?? (!selectorProvided ? await resolveDefaultWorkspace() : null);
|
|
1275
1558
|
if (chosen) {
|
|
1276
1559
|
return {
|
|
1277
1560
|
client: new SlackApiClient(chosen.auth, {
|
|
@@ -1282,16 +1565,38 @@ async function getClientForWorkspace(workspaceUrl) {
|
|
|
1282
1565
|
};
|
|
1283
1566
|
}
|
|
1284
1567
|
} catch {}
|
|
1285
|
-
const chrome = extractFromChrome();
|
|
1568
|
+
const chrome = extractFromChrome() ?? await extractFromBrave();
|
|
1286
1569
|
if (chrome && chrome.teams.length > 0) {
|
|
1287
|
-
|
|
1570
|
+
let chosen = chrome.teams[0];
|
|
1571
|
+
if (selector) {
|
|
1572
|
+
const normalizedSelector = selector.toLowerCase();
|
|
1573
|
+
const matches = chrome.teams.filter((t) => {
|
|
1574
|
+
const normalizedUrl = normalizeUrl2(t.url).toLowerCase();
|
|
1575
|
+
const host = new URL(t.url).host.toLowerCase();
|
|
1576
|
+
const hostWithoutSlackSuffix = host.replace(/\.slack\.com$/i, "");
|
|
1577
|
+
const name = t.name?.toLowerCase() ?? "";
|
|
1578
|
+
return normalizedUrl.includes(normalizedSelector) || host.includes(normalizedSelector) || hostWithoutSlackSuffix.includes(normalizedSelector) || name.includes(normalizedSelector);
|
|
1579
|
+
});
|
|
1580
|
+
if (matches.length > 1) {
|
|
1581
|
+
throw new Error(`Workspace selector "${selector}" is ambiguous in Chrome workspaces. Matches: ${matches.map((t) => normalizeUrl2(t.url)).join(", ")}. Pass a more specific selector or full workspace URL.`);
|
|
1582
|
+
}
|
|
1583
|
+
if (matches.length === 1) {
|
|
1584
|
+
chosen = matches[0];
|
|
1585
|
+
} else if (normalizedSelectorUrl) {
|
|
1586
|
+
try {
|
|
1587
|
+
chosen = chrome.teams.find((t) => normalizeUrl2(t.url) === normalizeUrl2(selector)) ?? chosen;
|
|
1588
|
+
} catch {}
|
|
1589
|
+
} else {
|
|
1590
|
+
throw new Error(`No configured workspace matches selector "${selector}". Run "agent-slack auth whoami" to list available workspaces.`);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1288
1593
|
const auth = {
|
|
1289
1594
|
auth_type: "browser",
|
|
1290
1595
|
xoxc_token: chosen.token,
|
|
1291
1596
|
xoxd_cookie: chrome.cookie_d
|
|
1292
1597
|
};
|
|
1293
1598
|
await upsertWorkspace({
|
|
1294
|
-
workspace_url:
|
|
1599
|
+
workspace_url: normalizeUrl2(chosen.url),
|
|
1295
1600
|
workspace_name: chosen.name,
|
|
1296
1601
|
auth: {
|
|
1297
1602
|
auth_type: "browser",
|
|
@@ -1301,13 +1606,16 @@ async function getClientForWorkspace(workspaceUrl) {
|
|
|
1301
1606
|
});
|
|
1302
1607
|
return {
|
|
1303
1608
|
client: new SlackApiClient(auth, {
|
|
1304
|
-
workspaceUrl:
|
|
1609
|
+
workspaceUrl: normalizeUrl2(chosen.url)
|
|
1305
1610
|
}),
|
|
1306
1611
|
auth,
|
|
1307
|
-
workspace_url:
|
|
1612
|
+
workspace_url: normalizeUrl2(chosen.url)
|
|
1308
1613
|
};
|
|
1309
1614
|
}
|
|
1310
|
-
|
|
1615
|
+
if (selector && !normalizedSelectorUrl) {
|
|
1616
|
+
throw new Error(`No configured workspace matches selector "${selector}". Run "agent-slack auth whoami" to list available workspaces.`);
|
|
1617
|
+
}
|
|
1618
|
+
throw new Error('No Slack credentials available. Try "agent-slack auth import-desktop", "agent-slack auth import-chrome", "agent-slack auth import-brave", or set SLACK_TOKEN / SLACK_COOKIE_D.');
|
|
1311
1619
|
}
|
|
1312
1620
|
function createCliContext() {
|
|
1313
1621
|
return {
|
|
@@ -1315,12 +1623,13 @@ function createCliContext() {
|
|
|
1315
1623
|
assertWorkspaceSpecifiedForChannelNames,
|
|
1316
1624
|
withAutoRefresh,
|
|
1317
1625
|
getClientForWorkspace,
|
|
1318
|
-
normalizeUrl,
|
|
1626
|
+
normalizeUrl: normalizeUrl2,
|
|
1319
1627
|
errorMessage,
|
|
1320
1628
|
parseContentType,
|
|
1321
1629
|
parseCurl: parseSlackCurlCommand,
|
|
1322
1630
|
importDesktop: extractFromSlackDesktop,
|
|
1323
|
-
importChrome: extractFromChrome
|
|
1631
|
+
importChrome: extractFromChrome,
|
|
1632
|
+
importBrave: extractFromBrave
|
|
1324
1633
|
};
|
|
1325
1634
|
}
|
|
1326
1635
|
|
|
@@ -1400,7 +1709,7 @@ function registerAuthCommand(input) {
|
|
|
1400
1709
|
process.exitCode = 1;
|
|
1401
1710
|
}
|
|
1402
1711
|
});
|
|
1403
|
-
auth.command("test").description("Verify credentials (calls Slack auth.test)").option("--workspace <url>", "Workspace URL
|
|
1712
|
+
auth.command("test").description("Verify credentials (calls Slack auth.test)").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when you have multiple workspaces)").action(async (...args) => {
|
|
1404
1713
|
const [options] = args;
|
|
1405
1714
|
try {
|
|
1406
1715
|
const resp = await runAuthTest({
|
|
@@ -1436,6 +1745,29 @@ function registerAuthCommand(input) {
|
|
|
1436
1745
|
process.exitCode = 1;
|
|
1437
1746
|
}
|
|
1438
1747
|
});
|
|
1748
|
+
auth.command("import-brave").description("Import xoxc/xoxd from a logged-in Slack tab in Brave Browser (macOS)").action(async () => {
|
|
1749
|
+
try {
|
|
1750
|
+
const extracted = await input.ctx.importBrave();
|
|
1751
|
+
if (!extracted) {
|
|
1752
|
+
throw new Error("Could not extract tokens from Brave. Open Slack in Brave and ensure you're logged in.");
|
|
1753
|
+
}
|
|
1754
|
+
for (const team of extracted.teams) {
|
|
1755
|
+
await upsertWorkspace({
|
|
1756
|
+
workspace_url: input.ctx.normalizeUrl(team.url),
|
|
1757
|
+
workspace_name: team.name,
|
|
1758
|
+
auth: {
|
|
1759
|
+
auth_type: "browser",
|
|
1760
|
+
xoxc_token: team.token,
|
|
1761
|
+
xoxd_cookie: extracted.cookie_d
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
console.log(`Imported ${extracted.teams.length} workspace token(s) from Brave.`);
|
|
1766
|
+
} catch (err) {
|
|
1767
|
+
console.error(input.ctx.errorMessage(err));
|
|
1768
|
+
process.exitCode = 1;
|
|
1769
|
+
}
|
|
1770
|
+
});
|
|
1439
1771
|
auth.command("parse-curl").description("Paste a Slack API request copied as cURL (extracts xoxc/xoxd and saves locally)").action(async () => {
|
|
1440
1772
|
try {
|
|
1441
1773
|
const curlInput = await new Response(process.stdin).text();
|
|
@@ -1537,15 +1869,15 @@ function registerAuthCommand(input) {
|
|
|
1537
1869
|
|
|
1538
1870
|
// src/slack/files.ts
|
|
1539
1871
|
import { mkdir as mkdir3, writeFile as writeFile2 } from "node:fs/promises";
|
|
1540
|
-
import { basename, join as
|
|
1541
|
-
import { existsSync as
|
|
1872
|
+
import { basename, join as join6, resolve } from "node:path";
|
|
1873
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1542
1874
|
async function downloadSlackFile(input) {
|
|
1543
1875
|
const { auth, url, destDir, preferredName, options } = input;
|
|
1544
1876
|
const absDir = resolve(destDir);
|
|
1545
1877
|
await mkdir3(absDir, { recursive: true });
|
|
1546
1878
|
const name = sanitizeFilename(preferredName || basename(new URL(url).pathname) || "file");
|
|
1547
|
-
const path =
|
|
1548
|
-
if (
|
|
1879
|
+
const path = join6(absDir, name);
|
|
1880
|
+
if (existsSync4(path)) {
|
|
1549
1881
|
return path;
|
|
1550
1882
|
}
|
|
1551
1883
|
const headers = {};
|
|
@@ -1600,27 +1932,27 @@ function extractTag(html, tag) {
|
|
|
1600
1932
|
}
|
|
1601
1933
|
|
|
1602
1934
|
// src/lib/tmp-paths.ts
|
|
1603
|
-
import { join as
|
|
1935
|
+
import { join as join8, resolve as resolve2 } from "node:path";
|
|
1604
1936
|
import { mkdir as mkdir4 } from "node:fs/promises";
|
|
1605
1937
|
|
|
1606
1938
|
// src/lib/app-dir.ts
|
|
1607
|
-
import { homedir as
|
|
1608
|
-
import { join as
|
|
1939
|
+
import { homedir as homedir4, tmpdir } from "node:os";
|
|
1940
|
+
import { join as join7 } from "node:path";
|
|
1609
1941
|
function getAppDir() {
|
|
1610
1942
|
const xdg = process.env.XDG_RUNTIME_DIR?.trim();
|
|
1611
1943
|
if (xdg) {
|
|
1612
|
-
return
|
|
1944
|
+
return join7(xdg, "agent-slack");
|
|
1613
1945
|
}
|
|
1614
|
-
const home =
|
|
1946
|
+
const home = homedir4();
|
|
1615
1947
|
if (home) {
|
|
1616
|
-
return
|
|
1948
|
+
return join7(home, ".agent-slack");
|
|
1617
1949
|
}
|
|
1618
|
-
return
|
|
1950
|
+
return join7(tmpdir(), "agent-slack");
|
|
1619
1951
|
}
|
|
1620
1952
|
|
|
1621
1953
|
// src/lib/tmp-paths.ts
|
|
1622
1954
|
function getDownloadsDir() {
|
|
1623
|
-
return resolve2(
|
|
1955
|
+
return resolve2(join8(getAppDir(), "tmp", "downloads"));
|
|
1624
1956
|
}
|
|
1625
1957
|
async function ensureDownloadsDir() {
|
|
1626
1958
|
const dir = getDownloadsDir();
|
|
@@ -1653,12 +1985,12 @@ function parseSlackCanvasUrl(input) {
|
|
|
1653
1985
|
}
|
|
1654
1986
|
async function fetchCanvasMarkdown(client, input) {
|
|
1655
1987
|
const info = await client.api("files.info", { file: input.canvasId });
|
|
1656
|
-
const file =
|
|
1988
|
+
const file = isRecord5(info.file) ? info.file : null;
|
|
1657
1989
|
if (!file) {
|
|
1658
1990
|
throw new Error("Canvas not found (files.info returned no file)");
|
|
1659
1991
|
}
|
|
1660
|
-
const title = (
|
|
1661
|
-
const downloadUrl =
|
|
1992
|
+
const title = (getString(file.title) || getString(file.name) || "").trim() || undefined;
|
|
1993
|
+
const downloadUrl = getString(file.url_private_download) ?? getString(file.url_private);
|
|
1662
1994
|
if (!downloadUrl) {
|
|
1663
1995
|
throw new Error("Canvas has no download URL");
|
|
1664
1996
|
}
|
|
@@ -1700,17 +2032,11 @@ async function fetchCanvasMarkdown(client, input) {
|
|
|
1700
2032
|
}
|
|
1701
2033
|
};
|
|
1702
2034
|
}
|
|
1703
|
-
function isRecord6(value) {
|
|
1704
|
-
return typeof value === "object" && value !== null;
|
|
1705
|
-
}
|
|
1706
|
-
function getString2(value) {
|
|
1707
|
-
return typeof value === "string" ? value : undefined;
|
|
1708
|
-
}
|
|
1709
2035
|
|
|
1710
2036
|
// src/cli/canvas-command.ts
|
|
1711
2037
|
function registerCanvasCommand(input) {
|
|
1712
2038
|
const canvasCmd = input.program.command("canvas").description("Work with Slack canvases");
|
|
1713
|
-
canvasCmd.command("get").description("Fetch a Slack canvas and convert it to Markdown").argument("<canvas>", "Slack canvas URL (…/docs/…/F…) or canvas id (F…)").option("--workspace <url>", "Workspace URL
|
|
2039
|
+
canvasCmd.command("get").description("Fetch a Slack canvas and convert it to Markdown").argument("<canvas>", "Slack canvas URL (…/docs/…/F…) or canvas id (F…)").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if passing a canvas id across multiple workspaces)").option("--max-chars <n>", "Max markdown characters to include (default 20000, -1 for unlimited)", "20000").action(async (...args) => {
|
|
1714
2040
|
const [value, options] = args;
|
|
1715
2041
|
try {
|
|
1716
2042
|
let workspaceUrl;
|
|
@@ -1792,17 +2118,21 @@ function slackMrkdwnToMarkdown(text) {
|
|
|
1792
2118
|
}
|
|
1793
2119
|
|
|
1794
2120
|
// src/slack/render.ts
|
|
2121
|
+
var MAX_ATTACHMENT_DEPTH = 8;
|
|
1795
2122
|
function renderSlackMessageContent(msg) {
|
|
1796
2123
|
const msgObj = isRecord7(msg) ? msg : {};
|
|
1797
2124
|
const blockMrkdwn = extractMrkdwnFromBlocks(msgObj.blocks);
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
2125
|
+
const attachmentMrkdwn = extractMrkdwnFromAttachments(msgObj.attachments, {
|
|
2126
|
+
depth: 0,
|
|
2127
|
+
seen: new WeakSet
|
|
2128
|
+
});
|
|
2129
|
+
const combined = [blockMrkdwn.trim(), attachmentMrkdwn.trim()].filter(Boolean).join(`
|
|
2130
|
+
|
|
2131
|
+
`);
|
|
2132
|
+
if (combined) {
|
|
2133
|
+
return slackMrkdwnToMarkdown(combined).trim();
|
|
1804
2134
|
}
|
|
1805
|
-
const text =
|
|
2135
|
+
const text = getString2(msgObj.text).trim();
|
|
1806
2136
|
if (text) {
|
|
1807
2137
|
return slackMrkdwnToMarkdown(text).trim();
|
|
1808
2138
|
}
|
|
@@ -1817,28 +2147,28 @@ function extractMrkdwnFromBlocks(blocks) {
|
|
|
1817
2147
|
if (!isRecord7(b)) {
|
|
1818
2148
|
continue;
|
|
1819
2149
|
}
|
|
1820
|
-
const type =
|
|
2150
|
+
const type = getString2(b.type);
|
|
1821
2151
|
if (type === "section") {
|
|
1822
2152
|
const text = isRecord7(b.text) ? b.text : null;
|
|
1823
|
-
const textType = text ?
|
|
2153
|
+
const textType = text ? getString2(text.type) : "";
|
|
1824
2154
|
if (textType === "mrkdwn" || textType === "plain_text") {
|
|
1825
|
-
out.push(
|
|
2155
|
+
out.push(getString2(text?.text));
|
|
1826
2156
|
}
|
|
1827
2157
|
if (Array.isArray(b.fields)) {
|
|
1828
2158
|
for (const f of b.fields) {
|
|
1829
2159
|
if (!isRecord7(f)) {
|
|
1830
2160
|
continue;
|
|
1831
2161
|
}
|
|
1832
|
-
const fieldType =
|
|
2162
|
+
const fieldType = getString2(f.type);
|
|
1833
2163
|
if (fieldType === "mrkdwn" || fieldType === "plain_text") {
|
|
1834
|
-
out.push(
|
|
2164
|
+
out.push(getString2(f.text));
|
|
1835
2165
|
}
|
|
1836
2166
|
}
|
|
1837
2167
|
}
|
|
1838
2168
|
const accessory = isRecord7(b.accessory) ? b.accessory : null;
|
|
1839
|
-
if (
|
|
1840
|
-
const label =
|
|
1841
|
-
const url =
|
|
2169
|
+
if (getString2(accessory?.type) === "button") {
|
|
2170
|
+
const label = getString2(accessory?.text?.text);
|
|
2171
|
+
const url = getString2(accessory?.url);
|
|
1842
2172
|
if (url) {
|
|
1843
2173
|
out.push(label ? `${label}: ${url}` : url);
|
|
1844
2174
|
}
|
|
@@ -1850,9 +2180,9 @@ function extractMrkdwnFromBlocks(blocks) {
|
|
|
1850
2180
|
if (!isRecord7(el)) {
|
|
1851
2181
|
continue;
|
|
1852
2182
|
}
|
|
1853
|
-
if (
|
|
1854
|
-
const label =
|
|
1855
|
-
const url =
|
|
2183
|
+
if (getString2(el.type) === "button") {
|
|
2184
|
+
const label = getString2(el.text?.text);
|
|
2185
|
+
const url = getString2(el.url);
|
|
1856
2186
|
if (url) {
|
|
1857
2187
|
out.push(label ? `${label}: ${url}` : url);
|
|
1858
2188
|
}
|
|
@@ -1865,19 +2195,19 @@ function extractMrkdwnFromBlocks(blocks) {
|
|
|
1865
2195
|
if (!isRecord7(el)) {
|
|
1866
2196
|
continue;
|
|
1867
2197
|
}
|
|
1868
|
-
const elType =
|
|
2198
|
+
const elType = getString2(el.type);
|
|
1869
2199
|
if (elType === "mrkdwn") {
|
|
1870
|
-
out.push(
|
|
2200
|
+
out.push(getString2(el.text));
|
|
1871
2201
|
}
|
|
1872
2202
|
if (elType === "plain_text") {
|
|
1873
|
-
out.push(
|
|
2203
|
+
out.push(getString2(el.text));
|
|
1874
2204
|
}
|
|
1875
2205
|
}
|
|
1876
2206
|
continue;
|
|
1877
2207
|
}
|
|
1878
2208
|
if (type === "image") {
|
|
1879
|
-
const alt =
|
|
1880
|
-
const url =
|
|
2209
|
+
const alt = getString2(b.alt_text);
|
|
2210
|
+
const url = getString2(b.image_url);
|
|
1881
2211
|
if (url) {
|
|
1882
2212
|
out.push(alt ? `${alt}: ${url}` : url);
|
|
1883
2213
|
}
|
|
@@ -1915,7 +2245,7 @@ function extractMrkdwnFromRichTextElement(el) {
|
|
|
1915
2245
|
if (!isRecord7(el)) {
|
|
1916
2246
|
return "";
|
|
1917
2247
|
}
|
|
1918
|
-
const t =
|
|
2248
|
+
const t = getString2(el.type);
|
|
1919
2249
|
if (t === "rich_text_section") {
|
|
1920
2250
|
const parts = [];
|
|
1921
2251
|
for (const child of Array.isArray(el.elements) ? el.elements : []) {
|
|
@@ -1961,7 +2291,7 @@ function extractMrkdwnFromRichTextElement(el) {
|
|
|
1961
2291
|
`);
|
|
1962
2292
|
}
|
|
1963
2293
|
if (t === "text") {
|
|
1964
|
-
const raw =
|
|
2294
|
+
const raw = getString2(el.text);
|
|
1965
2295
|
const style = isRecord7(el.style) ? el.style : null;
|
|
1966
2296
|
if (!style) {
|
|
1967
2297
|
return raw;
|
|
@@ -1982,28 +2312,31 @@ function extractMrkdwnFromRichTextElement(el) {
|
|
|
1982
2312
|
return text;
|
|
1983
2313
|
}
|
|
1984
2314
|
if (t === "link") {
|
|
1985
|
-
const url =
|
|
1986
|
-
const text =
|
|
2315
|
+
const url = getString2(el.url);
|
|
2316
|
+
const text = getString2(el.text);
|
|
1987
2317
|
if (!url) {
|
|
1988
2318
|
return text;
|
|
1989
2319
|
}
|
|
1990
2320
|
return text ? `<${url}|${text}>` : url;
|
|
1991
2321
|
}
|
|
1992
2322
|
if (t === "emoji") {
|
|
1993
|
-
const name =
|
|
2323
|
+
const name = getString2(el.name);
|
|
1994
2324
|
return name ? `:${name}:` : "";
|
|
1995
2325
|
}
|
|
1996
2326
|
if (t === "user") {
|
|
1997
|
-
const userId =
|
|
2327
|
+
const userId = getString2(el.user_id);
|
|
1998
2328
|
return userId ? `<@${userId}>` : "";
|
|
1999
2329
|
}
|
|
2000
2330
|
if (t === "channel") {
|
|
2001
|
-
const channelId =
|
|
2331
|
+
const channelId = getString2(el.channel_id);
|
|
2002
2332
|
return channelId ? `<#${channelId}>` : "";
|
|
2003
2333
|
}
|
|
2004
2334
|
return "";
|
|
2005
2335
|
}
|
|
2006
|
-
function extractMrkdwnFromAttachments(attachments) {
|
|
2336
|
+
function extractMrkdwnFromAttachments(attachments, state) {
|
|
2337
|
+
if (state.depth >= MAX_ATTACHMENT_DEPTH) {
|
|
2338
|
+
return "";
|
|
2339
|
+
}
|
|
2007
2340
|
if (!Array.isArray(attachments)) {
|
|
2008
2341
|
return "";
|
|
2009
2342
|
}
|
|
@@ -2012,17 +2345,34 @@ function extractMrkdwnFromAttachments(attachments) {
|
|
|
2012
2345
|
if (!isRecord7(a)) {
|
|
2013
2346
|
continue;
|
|
2014
2347
|
}
|
|
2348
|
+
if (state.seen.has(a)) {
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
state.seen.add(a);
|
|
2352
|
+
const isSharedMessage = Boolean(a.is_share || a.is_msg_unfurl && Array.isArray(a.message_blocks));
|
|
2015
2353
|
const chunk = [];
|
|
2354
|
+
if (isSharedMessage) {
|
|
2355
|
+
chunk.push(formatForwardHeader(a));
|
|
2356
|
+
const body = extractForwardedMessageBody(a, state).trim() || extractMrkdwnFromAttachments(a.attachments, nextState(state)).trim() || getString2(a.text).trim();
|
|
2357
|
+
if (body) {
|
|
2358
|
+
chunk.push(quoteMarkdown(body));
|
|
2359
|
+
}
|
|
2360
|
+
if (chunk.length > 0) {
|
|
2361
|
+
parts.push(chunk.join(`
|
|
2362
|
+
`));
|
|
2363
|
+
}
|
|
2364
|
+
continue;
|
|
2365
|
+
}
|
|
2016
2366
|
const blocks = extractMrkdwnFromBlocks(a.blocks);
|
|
2017
2367
|
if (blocks.trim()) {
|
|
2018
2368
|
chunk.push(blocks);
|
|
2019
2369
|
}
|
|
2020
|
-
const pretext =
|
|
2370
|
+
const pretext = getString2(a.pretext);
|
|
2021
2371
|
if (pretext) {
|
|
2022
2372
|
chunk.push(pretext);
|
|
2023
2373
|
}
|
|
2024
|
-
const title =
|
|
2025
|
-
const titleLink =
|
|
2374
|
+
const title = getString2(a.title);
|
|
2375
|
+
const titleLink = getString2(a.title_link);
|
|
2026
2376
|
if (titleLink && title) {
|
|
2027
2377
|
chunk.push(`<${titleLink}|${title}>`);
|
|
2028
2378
|
} else if (title) {
|
|
@@ -2030,7 +2380,7 @@ function extractMrkdwnFromAttachments(attachments) {
|
|
|
2030
2380
|
} else if (titleLink) {
|
|
2031
2381
|
chunk.push(titleLink);
|
|
2032
2382
|
}
|
|
2033
|
-
const text =
|
|
2383
|
+
const text = getString2(a.text);
|
|
2034
2384
|
if (text) {
|
|
2035
2385
|
chunk.push(text);
|
|
2036
2386
|
}
|
|
@@ -2039,8 +2389,8 @@ function extractMrkdwnFromAttachments(attachments) {
|
|
|
2039
2389
|
if (!isRecord7(f)) {
|
|
2040
2390
|
continue;
|
|
2041
2391
|
}
|
|
2042
|
-
const fieldTitle =
|
|
2043
|
-
const value =
|
|
2392
|
+
const fieldTitle = getString2(f.title);
|
|
2393
|
+
const value = getString2(f.value);
|
|
2044
2394
|
if (fieldTitle && value) {
|
|
2045
2395
|
chunk.push(`${fieldTitle}
|
|
2046
2396
|
${value}`);
|
|
@@ -2051,26 +2401,174 @@ ${value}`);
|
|
|
2051
2401
|
}
|
|
2052
2402
|
}
|
|
2053
2403
|
}
|
|
2054
|
-
const fallback =
|
|
2404
|
+
const fallback = getString2(a.fallback);
|
|
2055
2405
|
if (chunk.length === 0 && fallback) {
|
|
2056
2406
|
chunk.push(fallback);
|
|
2057
2407
|
}
|
|
2408
|
+
const nestedAttachments = extractMrkdwnFromAttachments(a.attachments, nextState(state));
|
|
2409
|
+
if (nestedAttachments.trim()) {
|
|
2410
|
+
chunk.push(nestedAttachments);
|
|
2411
|
+
}
|
|
2058
2412
|
if (chunk.length > 0) {
|
|
2059
|
-
parts.push(chunk.join(`
|
|
2413
|
+
parts.push(uniqueTexts(chunk).join(`
|
|
2060
2414
|
`));
|
|
2061
2415
|
}
|
|
2062
2416
|
}
|
|
2063
|
-
return parts.join(`
|
|
2417
|
+
return uniqueTexts(parts).join(`
|
|
2418
|
+
|
|
2419
|
+
`);
|
|
2420
|
+
}
|
|
2421
|
+
function formatForwardHeader(a) {
|
|
2422
|
+
const authorName = getString2(a.author_name);
|
|
2423
|
+
const authorLink = getString2(a.author_link);
|
|
2424
|
+
const fromUrl = getString2(a.from_url);
|
|
2425
|
+
const authorPart = authorName && authorLink ? `<${authorLink}|${authorName}>` : authorName || "";
|
|
2426
|
+
const sourcePart = fromUrl ? `<${fromUrl}|original>` : "";
|
|
2427
|
+
if (authorPart && sourcePart) {
|
|
2428
|
+
return `*Forwarded from ${authorPart} | ${sourcePart}*`;
|
|
2429
|
+
}
|
|
2430
|
+
if (authorPart) {
|
|
2431
|
+
return `*Forwarded from ${authorPart}*`;
|
|
2432
|
+
}
|
|
2433
|
+
if (sourcePart) {
|
|
2434
|
+
return `*Forwarded message | ${sourcePart}*`;
|
|
2435
|
+
}
|
|
2436
|
+
return "*Forwarded message*";
|
|
2437
|
+
}
|
|
2438
|
+
function extractForwardedMessageBody(attachment, state) {
|
|
2439
|
+
const messageBlocks = attachment.message_blocks;
|
|
2440
|
+
const topLevelFiles = extractFileMentions(attachment.files).trim();
|
|
2441
|
+
if (!Array.isArray(messageBlocks)) {
|
|
2442
|
+
return topLevelFiles;
|
|
2443
|
+
}
|
|
2444
|
+
const out = [];
|
|
2445
|
+
for (const mb of messageBlocks) {
|
|
2446
|
+
if (!isRecord7(mb)) {
|
|
2447
|
+
continue;
|
|
2448
|
+
}
|
|
2449
|
+
const message = isRecord7(mb.message) ? mb.message : null;
|
|
2450
|
+
if (!message) {
|
|
2451
|
+
continue;
|
|
2452
|
+
}
|
|
2453
|
+
const messageText = getString2(message.text).trim();
|
|
2454
|
+
const blocksContent = extractMrkdwnFromBlocks(message.blocks).trim();
|
|
2455
|
+
const attachmentsContent = extractMrkdwnFromAttachments(message.attachments, nextState(state)).trim();
|
|
2456
|
+
const fileMentions = extractFileMentions(message.files).trim();
|
|
2457
|
+
const content = uniqueTexts([
|
|
2458
|
+
blocksContent,
|
|
2459
|
+
attachmentsContent,
|
|
2460
|
+
messageText,
|
|
2461
|
+
fileMentions
|
|
2462
|
+
]).join(`
|
|
2064
2463
|
|
|
2464
|
+
`);
|
|
2465
|
+
if (content) {
|
|
2466
|
+
out.push(content);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
return uniqueTexts([topLevelFiles, ...out]).join(`
|
|
2470
|
+
`);
|
|
2471
|
+
}
|
|
2472
|
+
function nextState(state) {
|
|
2473
|
+
return { depth: state.depth + 1, seen: state.seen };
|
|
2474
|
+
}
|
|
2475
|
+
function quoteMarkdown(text) {
|
|
2476
|
+
return text.split(`
|
|
2477
|
+
`).map((line) => `> ${line}`).join(`
|
|
2478
|
+
`);
|
|
2479
|
+
}
|
|
2480
|
+
function uniqueTexts(values) {
|
|
2481
|
+
const out = [];
|
|
2482
|
+
const seen = new Set;
|
|
2483
|
+
for (const value of values) {
|
|
2484
|
+
const text = value.trim();
|
|
2485
|
+
if (!text || seen.has(text)) {
|
|
2486
|
+
continue;
|
|
2487
|
+
}
|
|
2488
|
+
seen.add(text);
|
|
2489
|
+
out.push(text);
|
|
2490
|
+
}
|
|
2491
|
+
return out;
|
|
2492
|
+
}
|
|
2493
|
+
function extractFileMentions(files) {
|
|
2494
|
+
if (!Array.isArray(files)) {
|
|
2495
|
+
return "";
|
|
2496
|
+
}
|
|
2497
|
+
const lines = [];
|
|
2498
|
+
for (const f of files) {
|
|
2499
|
+
if (!isRecord7(f)) {
|
|
2500
|
+
continue;
|
|
2501
|
+
}
|
|
2502
|
+
const name = getString2(f.title) || getString2(f.name) || "file";
|
|
2503
|
+
const url = getString2(f.permalink) || getString2(f.url_private_download) || getString2(f.url_private);
|
|
2504
|
+
if (url) {
|
|
2505
|
+
lines.push(`<${url}|${name}>`);
|
|
2506
|
+
continue;
|
|
2507
|
+
}
|
|
2508
|
+
lines.push(name);
|
|
2509
|
+
}
|
|
2510
|
+
return uniqueTexts(lines).join(`
|
|
2065
2511
|
`);
|
|
2066
2512
|
}
|
|
2067
2513
|
function isRecord7(value) {
|
|
2068
2514
|
return typeof value === "object" && value !== null;
|
|
2069
2515
|
}
|
|
2070
|
-
function
|
|
2516
|
+
function getString2(value) {
|
|
2071
2517
|
return typeof value === "string" ? value : "";
|
|
2072
2518
|
}
|
|
2073
2519
|
|
|
2520
|
+
// src/slack/message-api-parsing.ts
|
|
2521
|
+
function toSlackFileSummary(value) {
|
|
2522
|
+
if (!isRecord5(value)) {
|
|
2523
|
+
return null;
|
|
2524
|
+
}
|
|
2525
|
+
const id = getString(value.id);
|
|
2526
|
+
if (!id) {
|
|
2527
|
+
return null;
|
|
2528
|
+
}
|
|
2529
|
+
return {
|
|
2530
|
+
id,
|
|
2531
|
+
name: getString(value.name),
|
|
2532
|
+
title: getString(value.title),
|
|
2533
|
+
mimetype: getString(value.mimetype),
|
|
2534
|
+
filetype: getString(value.filetype),
|
|
2535
|
+
mode: getString(value.mode),
|
|
2536
|
+
permalink: getString(value.permalink),
|
|
2537
|
+
url_private: getString(value.url_private),
|
|
2538
|
+
url_private_download: getString(value.url_private_download),
|
|
2539
|
+
size: getNumber(value.size)
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
async function enrichFiles(client, files) {
|
|
2543
|
+
const out = [];
|
|
2544
|
+
for (const f of files) {
|
|
2545
|
+
if (f.mode === "snippet" || !f.url_private_download) {
|
|
2546
|
+
try {
|
|
2547
|
+
const info = await client.api("files.info", { file: f.id });
|
|
2548
|
+
const file = isRecord5(info.file) ? info.file : null;
|
|
2549
|
+
out.push({
|
|
2550
|
+
...f,
|
|
2551
|
+
name: f.name ?? getString(file?.name),
|
|
2552
|
+
title: f.title ?? getString(file?.title),
|
|
2553
|
+
mimetype: f.mimetype ?? getString(file?.mimetype),
|
|
2554
|
+
filetype: f.filetype ?? getString(file?.filetype),
|
|
2555
|
+
mode: f.mode ?? getString(file?.mode),
|
|
2556
|
+
permalink: f.permalink ?? getString(file?.permalink),
|
|
2557
|
+
url_private: f.url_private ?? getString(file?.url_private),
|
|
2558
|
+
url_private_download: f.url_private_download ?? getString(file?.url_private_download),
|
|
2559
|
+
snippet: {
|
|
2560
|
+
content: getString(file?.content),
|
|
2561
|
+
language: getString(file?.filetype)
|
|
2562
|
+
}
|
|
2563
|
+
});
|
|
2564
|
+
continue;
|
|
2565
|
+
} catch {}
|
|
2566
|
+
}
|
|
2567
|
+
out.push(f);
|
|
2568
|
+
}
|
|
2569
|
+
return out;
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2074
2572
|
// src/slack/messages.ts
|
|
2075
2573
|
function toCompactMessage(msg, input) {
|
|
2076
2574
|
const maxBodyChars = input?.maxBodyChars ?? 8000;
|
|
@@ -2096,7 +2594,8 @@ function toCompactMessage(msg, input) {
|
|
|
2096
2594
|
author: msg.user || msg.bot_id ? { user_id: msg.user, bot_id: msg.bot_id } : undefined,
|
|
2097
2595
|
content: content ? content : undefined,
|
|
2098
2596
|
files: files && files.length > 0 ? files : undefined,
|
|
2099
|
-
reactions: includeReactions ? compactReactions(msg.reactions) : undefined
|
|
2597
|
+
reactions: includeReactions ? compactReactions(msg.reactions) : undefined,
|
|
2598
|
+
forwarded_threads: extractForwardedThreads(msg.attachments)
|
|
2100
2599
|
};
|
|
2101
2600
|
}
|
|
2102
2601
|
function compactReactions(reactions) {
|
|
@@ -2105,10 +2604,10 @@ function compactReactions(reactions) {
|
|
|
2105
2604
|
}
|
|
2106
2605
|
const out = [];
|
|
2107
2606
|
for (const r of reactions) {
|
|
2108
|
-
if (!
|
|
2607
|
+
if (!isRecord5(r)) {
|
|
2109
2608
|
continue;
|
|
2110
2609
|
}
|
|
2111
|
-
const name =
|
|
2610
|
+
const name = getString(r.name)?.trim() ?? "";
|
|
2112
2611
|
if (!name) {
|
|
2113
2612
|
continue;
|
|
2114
2613
|
}
|
|
@@ -2118,6 +2617,46 @@ function compactReactions(reactions) {
|
|
|
2118
2617
|
}
|
|
2119
2618
|
return out.length ? out : undefined;
|
|
2120
2619
|
}
|
|
2620
|
+
function extractForwardedThreads(attachments) {
|
|
2621
|
+
if (!Array.isArray(attachments) || attachments.length === 0) {
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
const out = [];
|
|
2625
|
+
const seen = new Set;
|
|
2626
|
+
for (const attachment of attachments) {
|
|
2627
|
+
if (!isRecord5(attachment)) {
|
|
2628
|
+
continue;
|
|
2629
|
+
}
|
|
2630
|
+
const fromUrl = getString(attachment.from_url)?.trim();
|
|
2631
|
+
if (!fromUrl) {
|
|
2632
|
+
continue;
|
|
2633
|
+
}
|
|
2634
|
+
let parsed;
|
|
2635
|
+
try {
|
|
2636
|
+
parsed = new URL(fromUrl);
|
|
2637
|
+
} catch {
|
|
2638
|
+
continue;
|
|
2639
|
+
}
|
|
2640
|
+
const threadTs = parsed.searchParams.get("thread_ts")?.trim();
|
|
2641
|
+
if (!threadTs || !/^\d{6,}\.\d{6}$/.test(threadTs)) {
|
|
2642
|
+
continue;
|
|
2643
|
+
}
|
|
2644
|
+
const channelId = parsed.searchParams.get("cid")?.trim();
|
|
2645
|
+
const replyCount = getNumber(attachment.reply_count);
|
|
2646
|
+
const key = `${fromUrl}::${threadTs}`;
|
|
2647
|
+
if (seen.has(key)) {
|
|
2648
|
+
continue;
|
|
2649
|
+
}
|
|
2650
|
+
seen.add(key);
|
|
2651
|
+
out.push({
|
|
2652
|
+
url: fromUrl,
|
|
2653
|
+
thread_ts: threadTs,
|
|
2654
|
+
channel_id: channelId || undefined,
|
|
2655
|
+
reply_count: replyCount
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
return out.length ? out : undefined;
|
|
2659
|
+
}
|
|
2121
2660
|
async function fetchMessage(client, input) {
|
|
2122
2661
|
const history = await client.api("conversations.history", {
|
|
2123
2662
|
channel: input.ref.channel_id,
|
|
@@ -2126,8 +2665,8 @@ async function fetchMessage(client, input) {
|
|
|
2126
2665
|
limit: 5,
|
|
2127
2666
|
include_all_metadata: input.includeReactions ? true : undefined
|
|
2128
2667
|
});
|
|
2129
|
-
const historyMessages =
|
|
2130
|
-
let msg = historyMessages.find((m) =>
|
|
2668
|
+
const historyMessages = asArray(history.messages);
|
|
2669
|
+
let msg = historyMessages.find((m) => isRecord5(m) && getString(m.ts) === input.ref.message_ts);
|
|
2131
2670
|
if (!msg && input.ref.thread_ts_hint) {
|
|
2132
2671
|
msg = await findMessageInThread(client, {
|
|
2133
2672
|
channelId: input.ref.channel_id,
|
|
@@ -2144,8 +2683,8 @@ async function fetchMessage(client, input) {
|
|
|
2144
2683
|
limit: 1,
|
|
2145
2684
|
include_all_metadata: input.includeReactions ? true : undefined
|
|
2146
2685
|
});
|
|
2147
|
-
const [root] =
|
|
2148
|
-
if (
|
|
2686
|
+
const [root] = asArray(rootResp.messages);
|
|
2687
|
+
if (isRecord5(root) && getString(root.ts) === input.ref.message_ts) {
|
|
2149
2688
|
msg = root;
|
|
2150
2689
|
}
|
|
2151
2690
|
} catch {}
|
|
@@ -2153,20 +2692,20 @@ async function fetchMessage(client, input) {
|
|
|
2153
2692
|
if (!msg) {
|
|
2154
2693
|
throw new Error("Message not found (no access or wrong URL)");
|
|
2155
2694
|
}
|
|
2156
|
-
const files =
|
|
2695
|
+
const files = asArray(msg.files).map((f) => toSlackFileSummary(f)).filter((f) => f !== null);
|
|
2157
2696
|
const enrichedFiles = files.length > 0 ? await enrichFiles(client, files) : undefined;
|
|
2158
|
-
const text =
|
|
2159
|
-
const ts =
|
|
2697
|
+
const text = getString(msg.text) ?? "";
|
|
2698
|
+
const ts = getString(msg.ts) ?? input.ref.message_ts;
|
|
2160
2699
|
const blocks = Array.isArray(msg.blocks) ? msg.blocks : undefined;
|
|
2161
2700
|
const attachments = Array.isArray(msg.attachments) ? msg.attachments : undefined;
|
|
2162
2701
|
const reactions = Array.isArray(msg.reactions) ? msg.reactions : undefined;
|
|
2163
2702
|
return {
|
|
2164
2703
|
channel_id: input.ref.channel_id,
|
|
2165
2704
|
ts,
|
|
2166
|
-
thread_ts:
|
|
2705
|
+
thread_ts: getString(msg.thread_ts),
|
|
2167
2706
|
reply_count: getNumber(msg.reply_count),
|
|
2168
|
-
user:
|
|
2169
|
-
bot_id:
|
|
2707
|
+
user: getString(msg.user),
|
|
2708
|
+
bot_id: getString(msg.bot_id),
|
|
2170
2709
|
text,
|
|
2171
2710
|
markdown: slackMrkdwnToMarkdown(text),
|
|
2172
2711
|
blocks,
|
|
@@ -2185,19 +2724,105 @@ async function findMessageInThread(client, input) {
|
|
|
2185
2724
|
cursor,
|
|
2186
2725
|
include_all_metadata: input.includeReactions ? true : undefined
|
|
2187
2726
|
});
|
|
2188
|
-
const messages =
|
|
2189
|
-
const found = messages.find((m) =>
|
|
2727
|
+
const messages = asArray(resp.messages);
|
|
2728
|
+
const found = messages.find((m) => isRecord5(m) && getString(m.ts) === input.targetTs);
|
|
2190
2729
|
if (found) {
|
|
2191
2730
|
return found;
|
|
2192
2731
|
}
|
|
2193
|
-
const meta =
|
|
2194
|
-
const next = meta ?
|
|
2195
|
-
if (!next) {
|
|
2732
|
+
const meta = isRecord5(resp.response_metadata) ? resp.response_metadata : null;
|
|
2733
|
+
const next = meta ? getString(meta.next_cursor) : undefined;
|
|
2734
|
+
if (!next) {
|
|
2735
|
+
break;
|
|
2736
|
+
}
|
|
2737
|
+
cursor = next;
|
|
2738
|
+
}
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
async function fetchChannelHistory(client, input) {
|
|
2742
|
+
const raw = input.limit ?? 25;
|
|
2743
|
+
const limit = Number.isFinite(raw) ? Math.min(Math.max(raw, 1), 200) : 25;
|
|
2744
|
+
const out = [];
|
|
2745
|
+
const withReactions = input.withReactions ?? [];
|
|
2746
|
+
const withoutReactions = input.withoutReactions ?? [];
|
|
2747
|
+
const hasReactionFilters = withReactions.length > 0 || withoutReactions.length > 0;
|
|
2748
|
+
const pageLimit = hasReactionFilters ? 200 : limit;
|
|
2749
|
+
let cursorLatest = input.latest;
|
|
2750
|
+
for (;; ) {
|
|
2751
|
+
const resp = await client.api("conversations.history", {
|
|
2752
|
+
channel: input.channelId,
|
|
2753
|
+
limit: pageLimit,
|
|
2754
|
+
latest: cursorLatest,
|
|
2755
|
+
oldest: input.oldest,
|
|
2756
|
+
include_all_metadata: input.includeReactions || hasReactionFilters ? true : undefined
|
|
2757
|
+
});
|
|
2758
|
+
const messages = asArray(resp.messages);
|
|
2759
|
+
if (messages.length === 0) {
|
|
2760
|
+
break;
|
|
2761
|
+
}
|
|
2762
|
+
for (const m of messages) {
|
|
2763
|
+
if (!isRecord5(m)) {
|
|
2764
|
+
continue;
|
|
2765
|
+
}
|
|
2766
|
+
if (hasReactionFilters && !passesReactionNameFilters(m, {
|
|
2767
|
+
withReactions,
|
|
2768
|
+
withoutReactions
|
|
2769
|
+
})) {
|
|
2770
|
+
continue;
|
|
2771
|
+
}
|
|
2772
|
+
const files = asArray(m.files).map((f) => toSlackFileSummary(f)).filter((f) => f !== null);
|
|
2773
|
+
const enrichedFiles = files.length > 0 ? await enrichFiles(client, files) : undefined;
|
|
2774
|
+
const text = getString(m.text) ?? "";
|
|
2775
|
+
out.push({
|
|
2776
|
+
channel_id: input.channelId,
|
|
2777
|
+
ts: getString(m.ts) ?? "",
|
|
2778
|
+
thread_ts: getString(m.thread_ts),
|
|
2779
|
+
reply_count: getNumber(m.reply_count),
|
|
2780
|
+
user: getString(m.user),
|
|
2781
|
+
bot_id: getString(m.bot_id),
|
|
2782
|
+
text,
|
|
2783
|
+
markdown: slackMrkdwnToMarkdown(text),
|
|
2784
|
+
blocks: Array.isArray(m.blocks) ? m.blocks : undefined,
|
|
2785
|
+
attachments: Array.isArray(m.attachments) ? m.attachments : undefined,
|
|
2786
|
+
files: enrichedFiles,
|
|
2787
|
+
reactions: Array.isArray(m.reactions) ? m.reactions : undefined
|
|
2788
|
+
});
|
|
2789
|
+
if (out.length >= limit) {
|
|
2790
|
+
break;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
if (out.length >= limit || !hasReactionFilters) {
|
|
2196
2794
|
break;
|
|
2197
2795
|
}
|
|
2198
|
-
|
|
2796
|
+
const last = messages.at(-1);
|
|
2797
|
+
const nextLatest = isRecord5(last) ? getString(last.ts) : undefined;
|
|
2798
|
+
if (!nextLatest || nextLatest === cursorLatest) {
|
|
2799
|
+
break;
|
|
2800
|
+
}
|
|
2801
|
+
cursorLatest = nextLatest;
|
|
2199
2802
|
}
|
|
2200
|
-
|
|
2803
|
+
out.sort((a, b) => Number.parseFloat(a.ts) - Number.parseFloat(b.ts));
|
|
2804
|
+
return out;
|
|
2805
|
+
}
|
|
2806
|
+
function passesReactionNameFilters(msg, input) {
|
|
2807
|
+
const withReactions = input.withReactions ?? [];
|
|
2808
|
+
const withoutReactions = input.withoutReactions ?? [];
|
|
2809
|
+
const names = new Set;
|
|
2810
|
+
for (const r of asArray(msg.reactions)) {
|
|
2811
|
+
if (!isRecord5(r)) {
|
|
2812
|
+
continue;
|
|
2813
|
+
}
|
|
2814
|
+
const name = getString(r.name)?.trim();
|
|
2815
|
+
if (name) {
|
|
2816
|
+
names.add(name);
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
if (withReactions.some((name) => !names.has(name))) {
|
|
2820
|
+
return false;
|
|
2821
|
+
}
|
|
2822
|
+
if (withoutReactions.some((name) => names.has(name))) {
|
|
2823
|
+
return false;
|
|
2824
|
+
}
|
|
2825
|
+
return true;
|
|
2201
2826
|
}
|
|
2202
2827
|
async function fetchThread(client, input) {
|
|
2203
2828
|
const out = [];
|
|
@@ -2210,21 +2835,21 @@ async function fetchThread(client, input) {
|
|
|
2210
2835
|
cursor,
|
|
2211
2836
|
include_all_metadata: input.includeReactions ? true : undefined
|
|
2212
2837
|
});
|
|
2213
|
-
const messages =
|
|
2838
|
+
const messages = asArray(resp.messages);
|
|
2214
2839
|
for (const m of messages) {
|
|
2215
|
-
if (!
|
|
2840
|
+
if (!isRecord5(m)) {
|
|
2216
2841
|
continue;
|
|
2217
2842
|
}
|
|
2218
|
-
const files =
|
|
2843
|
+
const files = asArray(m.files).map((f) => toSlackFileSummary(f)).filter((f) => f !== null);
|
|
2219
2844
|
const enrichedFiles = files.length > 0 ? await enrichFiles(client, files) : undefined;
|
|
2220
|
-
const text =
|
|
2845
|
+
const text = getString(m.text) ?? "";
|
|
2221
2846
|
out.push({
|
|
2222
2847
|
channel_id: input.channelId,
|
|
2223
|
-
ts:
|
|
2224
|
-
thread_ts:
|
|
2848
|
+
ts: getString(m.ts) ?? "",
|
|
2849
|
+
thread_ts: getString(m.thread_ts),
|
|
2225
2850
|
reply_count: getNumber(m.reply_count),
|
|
2226
|
-
user:
|
|
2227
|
-
bot_id:
|
|
2851
|
+
user: getString(m.user),
|
|
2852
|
+
bot_id: getString(m.bot_id),
|
|
2228
2853
|
text,
|
|
2229
2854
|
markdown: slackMrkdwnToMarkdown(text),
|
|
2230
2855
|
blocks: Array.isArray(m.blocks) ? m.blocks : undefined,
|
|
@@ -2233,8 +2858,8 @@ async function fetchThread(client, input) {
|
|
|
2233
2858
|
reactions: Array.isArray(m.reactions) ? m.reactions : undefined
|
|
2234
2859
|
});
|
|
2235
2860
|
}
|
|
2236
|
-
const meta =
|
|
2237
|
-
const next = meta ?
|
|
2861
|
+
const meta = isRecord5(resp.response_metadata) ? resp.response_metadata : null;
|
|
2862
|
+
const next = meta ? getString(meta.next_cursor) : undefined;
|
|
2238
2863
|
if (!next) {
|
|
2239
2864
|
break;
|
|
2240
2865
|
}
|
|
@@ -2243,68 +2868,6 @@ async function fetchThread(client, input) {
|
|
|
2243
2868
|
out.sort((a, b) => Number.parseFloat(a.ts) - Number.parseFloat(b.ts));
|
|
2244
2869
|
return out;
|
|
2245
2870
|
}
|
|
2246
|
-
async function enrichFiles(client, files) {
|
|
2247
|
-
const out = [];
|
|
2248
|
-
for (const f of files) {
|
|
2249
|
-
if (f.mode === "snippet" || !f.url_private_download) {
|
|
2250
|
-
try {
|
|
2251
|
-
const info = await client.api("files.info", { file: f.id });
|
|
2252
|
-
const file = isRecord8(info.file) ? info.file : null;
|
|
2253
|
-
out.push({
|
|
2254
|
-
...f,
|
|
2255
|
-
name: f.name ?? getString4(file?.name),
|
|
2256
|
-
title: f.title ?? getString4(file?.title),
|
|
2257
|
-
mimetype: f.mimetype ?? getString4(file?.mimetype),
|
|
2258
|
-
filetype: f.filetype ?? getString4(file?.filetype),
|
|
2259
|
-
mode: f.mode ?? getString4(file?.mode),
|
|
2260
|
-
permalink: f.permalink ?? getString4(file?.permalink),
|
|
2261
|
-
url_private: f.url_private ?? getString4(file?.url_private),
|
|
2262
|
-
url_private_download: f.url_private_download ?? getString4(file?.url_private_download),
|
|
2263
|
-
snippet: {
|
|
2264
|
-
content: getString4(file?.content),
|
|
2265
|
-
language: getString4(file?.filetype)
|
|
2266
|
-
}
|
|
2267
|
-
});
|
|
2268
|
-
continue;
|
|
2269
|
-
} catch {}
|
|
2270
|
-
}
|
|
2271
|
-
out.push(f);
|
|
2272
|
-
}
|
|
2273
|
-
return out;
|
|
2274
|
-
}
|
|
2275
|
-
function isRecord8(value) {
|
|
2276
|
-
return typeof value === "object" && value !== null;
|
|
2277
|
-
}
|
|
2278
|
-
function getString4(value) {
|
|
2279
|
-
return typeof value === "string" ? value : undefined;
|
|
2280
|
-
}
|
|
2281
|
-
function getNumber(value) {
|
|
2282
|
-
return typeof value === "number" ? value : undefined;
|
|
2283
|
-
}
|
|
2284
|
-
function asArray2(value) {
|
|
2285
|
-
return Array.isArray(value) ? value : [];
|
|
2286
|
-
}
|
|
2287
|
-
function toSlackFileSummary(value) {
|
|
2288
|
-
if (!isRecord8(value)) {
|
|
2289
|
-
return null;
|
|
2290
|
-
}
|
|
2291
|
-
const id = getString4(value.id);
|
|
2292
|
-
if (!id) {
|
|
2293
|
-
return null;
|
|
2294
|
-
}
|
|
2295
|
-
return {
|
|
2296
|
-
id,
|
|
2297
|
-
name: getString4(value.name),
|
|
2298
|
-
title: getString4(value.title),
|
|
2299
|
-
mimetype: getString4(value.mimetype),
|
|
2300
|
-
filetype: getString4(value.filetype),
|
|
2301
|
-
mode: getString4(value.mode),
|
|
2302
|
-
permalink: getString4(value.permalink),
|
|
2303
|
-
url_private: getString4(value.url_private),
|
|
2304
|
-
url_private_download: getString4(value.url_private_download),
|
|
2305
|
-
size: getNumber(value.size)
|
|
2306
|
-
};
|
|
2307
|
-
}
|
|
2308
2871
|
|
|
2309
2872
|
// src/slack/url.ts
|
|
2310
2873
|
function parseSlackMessageUrl(input) {
|
|
@@ -2361,38 +2924,10 @@ function parseMsgTarget(input) {
|
|
|
2361
2924
|
return { kind: "channel", channel: `#${trimmed}` };
|
|
2362
2925
|
}
|
|
2363
2926
|
|
|
2364
|
-
// src/cli/message-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
function asArray3(value) {
|
|
2369
|
-
return Array.isArray(value) ? value : [];
|
|
2370
|
-
}
|
|
2371
|
-
function getNumber2(value) {
|
|
2372
|
-
return typeof value === "number" ? value : undefined;
|
|
2373
|
-
}
|
|
2374
|
-
async function getThreadSummary(client, input) {
|
|
2375
|
-
const replyCount = input.msg.reply_count ?? 0;
|
|
2376
|
-
const rootTs = input.msg.thread_ts ?? (replyCount > 0 ? input.msg.ts : null);
|
|
2377
|
-
if (!rootTs) {
|
|
2378
|
-
return null;
|
|
2379
|
-
}
|
|
2380
|
-
if (!input.msg.thread_ts && replyCount > 0) {
|
|
2381
|
-
return { ts: rootTs, length: 1 + replyCount };
|
|
2382
|
-
}
|
|
2383
|
-
const resp = await client.api("conversations.replies", {
|
|
2384
|
-
channel: input.channelId,
|
|
2385
|
-
ts: rootTs,
|
|
2386
|
-
limit: 1
|
|
2387
|
-
});
|
|
2388
|
-
const [root] = asArray3(isRecord9(resp) ? resp.messages : undefined);
|
|
2389
|
-
const rootReplyCount = isRecord9(root) ? getNumber2(root.reply_count) : undefined;
|
|
2390
|
-
if (rootReplyCount === undefined) {
|
|
2391
|
-
return { ts: rootTs, length: 1 };
|
|
2392
|
-
}
|
|
2393
|
-
return { ts: rootTs, length: 1 + rootReplyCount };
|
|
2394
|
-
}
|
|
2395
|
-
function inferExt(file) {
|
|
2927
|
+
// src/cli/message-file-downloads.ts
|
|
2928
|
+
import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
2929
|
+
import { join as join9 } from "node:path";
|
|
2930
|
+
function inferFileExtension(file) {
|
|
2396
2931
|
const mt = (file.mimetype || "").toLowerCase();
|
|
2397
2932
|
const ft = (file.filetype || "").toLowerCase();
|
|
2398
2933
|
if (mt === "image/png" || ft === "png") {
|
|
@@ -2417,42 +2952,148 @@ function inferExt(file) {
|
|
|
2417
2952
|
return "json";
|
|
2418
2953
|
}
|
|
2419
2954
|
const name = file.name || file.title || "";
|
|
2420
|
-
const
|
|
2421
|
-
return
|
|
2955
|
+
const match = name.match(/\.([A-Za-z0-9]{1,10})$/);
|
|
2956
|
+
return match ? match[1].toLowerCase() : null;
|
|
2957
|
+
}
|
|
2958
|
+
var CANVAS_MODES = new Set(["canvas", "quip", "docs"]);
|
|
2959
|
+
function looksLikeAuthPage(html) {
|
|
2960
|
+
return /<form[^>]+signin|data-qa="signin|<title>[^<]*Sign\s*in/i.test(html);
|
|
2961
|
+
}
|
|
2962
|
+
async function downloadCanvasAsMarkdown(input) {
|
|
2963
|
+
const htmlPath = await downloadSlackFile({
|
|
2964
|
+
auth: input.auth,
|
|
2965
|
+
url: input.url,
|
|
2966
|
+
destDir: input.destDir,
|
|
2967
|
+
preferredName: `${input.fileId}.html`,
|
|
2968
|
+
options: { allowHtml: true }
|
|
2969
|
+
});
|
|
2970
|
+
const html = await readFile4(htmlPath, "utf8");
|
|
2971
|
+
if (looksLikeAuthPage(html)) {
|
|
2972
|
+
throw new Error("Downloaded auth/login page instead of canvas content (token may be expired)");
|
|
2973
|
+
}
|
|
2974
|
+
const markdown = htmlToMarkdown(html).trim();
|
|
2975
|
+
const safeName = `${input.fileId.replace(/[\\/<>"|?*]/g, "_")}.md`;
|
|
2976
|
+
const markdownPath = join9(input.destDir, safeName);
|
|
2977
|
+
await writeFile3(markdownPath, markdown, "utf8");
|
|
2978
|
+
return markdownPath;
|
|
2422
2979
|
}
|
|
2423
|
-
async function
|
|
2980
|
+
async function downloadMessageFiles(input) {
|
|
2424
2981
|
const downloadedPaths = {};
|
|
2425
2982
|
const downloadsDir = await ensureDownloadsDir();
|
|
2426
|
-
for (const
|
|
2427
|
-
for (const
|
|
2428
|
-
if (downloadedPaths[
|
|
2983
|
+
for (const message of input.messages) {
|
|
2984
|
+
for (const file of message.files ?? []) {
|
|
2985
|
+
if (downloadedPaths[file.id]) {
|
|
2429
2986
|
continue;
|
|
2430
2987
|
}
|
|
2431
|
-
const
|
|
2988
|
+
const isCanvas = file.mode != null && CANVAS_MODES.has(file.mode);
|
|
2989
|
+
const url = isCanvas ? file.url_private || file.url_private_download : file.url_private_download || file.url_private;
|
|
2432
2990
|
if (!url) {
|
|
2433
2991
|
continue;
|
|
2434
2992
|
}
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2993
|
+
try {
|
|
2994
|
+
if (isCanvas) {
|
|
2995
|
+
downloadedPaths[file.id] = await downloadCanvasAsMarkdown({
|
|
2996
|
+
auth: input.auth,
|
|
2997
|
+
fileId: file.id,
|
|
2998
|
+
url,
|
|
2999
|
+
destDir: downloadsDir
|
|
3000
|
+
});
|
|
3001
|
+
} else {
|
|
3002
|
+
const ext = inferFileExtension(file);
|
|
3003
|
+
downloadedPaths[file.id] = await downloadSlackFile({
|
|
3004
|
+
auth: input.auth,
|
|
3005
|
+
url,
|
|
3006
|
+
destDir: downloadsDir,
|
|
3007
|
+
preferredName: `${file.id}${ext ? `.${ext}` : ""}`
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
3010
|
+
} catch (err) {
|
|
3011
|
+
console.error(`Warning: skipping file ${file.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3012
|
+
}
|
|
2443
3013
|
}
|
|
2444
3014
|
}
|
|
2445
3015
|
return downloadedPaths;
|
|
2446
3016
|
}
|
|
3017
|
+
|
|
3018
|
+
// src/cli/message-url-warning.ts
|
|
3019
|
+
function warnOnTruncatedSlackUrl(ref) {
|
|
3020
|
+
if (ref.possiblyTruncated) {
|
|
3021
|
+
console.error(`Hint: URL may have been truncated by shell. Quote URLs containing "&":
|
|
3022
|
+
` + ' agent-slack message get "https://...?thread_ts=...&cid=..."');
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
// src/cli/message-thread-info.ts
|
|
3027
|
+
function getNumber2(value) {
|
|
3028
|
+
return typeof value === "number" ? value : undefined;
|
|
3029
|
+
}
|
|
3030
|
+
async function getThreadSummary(client, input) {
|
|
3031
|
+
const replyCount = input.msg.reply_count ?? 0;
|
|
3032
|
+
const rootTs = input.msg.thread_ts ?? (replyCount > 0 ? input.msg.ts : null);
|
|
3033
|
+
if (!rootTs) {
|
|
3034
|
+
return null;
|
|
3035
|
+
}
|
|
3036
|
+
if (!input.msg.thread_ts && replyCount > 0) {
|
|
3037
|
+
return { ts: rootTs, length: 1 + replyCount };
|
|
3038
|
+
}
|
|
3039
|
+
const resp = await client.api("conversations.replies", {
|
|
3040
|
+
channel: input.channelId,
|
|
3041
|
+
ts: rootTs,
|
|
3042
|
+
limit: 1
|
|
3043
|
+
});
|
|
3044
|
+
const [root] = asArray(isRecord5(resp) ? resp.messages : undefined);
|
|
3045
|
+
const rootReplyCount = isRecord5(root) ? getNumber2(root.reply_count) : undefined;
|
|
3046
|
+
if (rootReplyCount === undefined) {
|
|
3047
|
+
return { ts: rootTs, length: 1 };
|
|
3048
|
+
}
|
|
3049
|
+
return { ts: rootTs, length: 1 + rootReplyCount };
|
|
3050
|
+
}
|
|
2447
3051
|
function toThreadListMessage(m) {
|
|
2448
3052
|
const { channel_id: _channelId, thread_ts: _threadTs, ...rest } = m;
|
|
2449
3053
|
return rest;
|
|
2450
3054
|
}
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
3055
|
+
|
|
3056
|
+
// src/cli/message-actions.ts
|
|
3057
|
+
function parseLimit(raw) {
|
|
3058
|
+
if (raw === undefined) {
|
|
3059
|
+
return;
|
|
3060
|
+
}
|
|
3061
|
+
const n = Number.parseInt(raw, 10);
|
|
3062
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
3063
|
+
throw new Error(`Invalid --limit value "${raw}": must be a positive integer`);
|
|
3064
|
+
}
|
|
3065
|
+
return n;
|
|
3066
|
+
}
|
|
3067
|
+
function requireMessageTs(raw) {
|
|
3068
|
+
const ts = raw?.trim();
|
|
3069
|
+
if (!ts) {
|
|
3070
|
+
throw new Error('When targeting a channel, you must pass --ts "<seconds>.<micros>"');
|
|
3071
|
+
}
|
|
3072
|
+
return ts;
|
|
3073
|
+
}
|
|
3074
|
+
function parseReactionFilters(raw) {
|
|
3075
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
3076
|
+
return [];
|
|
3077
|
+
}
|
|
3078
|
+
const out = [];
|
|
3079
|
+
for (const value of raw) {
|
|
3080
|
+
const normalized = normalizeSlackReactionName(String(value));
|
|
3081
|
+
if (!out.includes(normalized)) {
|
|
3082
|
+
out.push(normalized);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
return out;
|
|
3086
|
+
}
|
|
3087
|
+
function requireOldestWhenReactionFiltersUsed(input) {
|
|
3088
|
+
const hasReactionFilters = input.withReactions.length > 0 || input.withoutReactions.length > 0;
|
|
3089
|
+
const oldest = input.oldest?.trim();
|
|
3090
|
+
if (!hasReactionFilters) {
|
|
3091
|
+
return oldest;
|
|
3092
|
+
}
|
|
3093
|
+
if (!oldest) {
|
|
3094
|
+
throw new Error('Reaction filters require --oldest "<seconds>.<micros>" to bound scan size. Example: --oldest "1770165109.628379"');
|
|
2455
3095
|
}
|
|
3096
|
+
return oldest;
|
|
2456
3097
|
}
|
|
2457
3098
|
async function handleMessageGet(input) {
|
|
2458
3099
|
const target = parseMsgTarget(input.targetInput);
|
|
@@ -2462,12 +3103,12 @@ async function handleMessageGet(input) {
|
|
|
2462
3103
|
work: async () => {
|
|
2463
3104
|
if (target.kind === "url") {
|
|
2464
3105
|
const { ref: ref2 } = target;
|
|
2465
|
-
|
|
3106
|
+
warnOnTruncatedSlackUrl(ref2);
|
|
2466
3107
|
const { client: client2, auth: auth2 } = await input.ctx.getClientForWorkspace(ref2.workspace_url);
|
|
2467
3108
|
const includeReactions2 = Boolean(input.options.includeReactions);
|
|
2468
3109
|
const msg2 = await fetchMessage(client2, { ref: ref2, includeReactions: includeReactions2 });
|
|
2469
3110
|
const thread2 = await getThreadSummary(client2, { channelId: ref2.channel_id, msg: msg2 });
|
|
2470
|
-
const downloadedPaths2 = await
|
|
3111
|
+
const downloadedPaths2 = await downloadMessageFiles({ auth: auth2, messages: [msg2] });
|
|
2471
3112
|
const maxBodyChars2 = Number.parseInt(input.options.maxBodyChars, 10);
|
|
2472
3113
|
const message2 = toCompactMessage(msg2, { maxBodyChars: maxBodyChars2, includeReactions: includeReactions2, downloadedPaths: downloadedPaths2 });
|
|
2473
3114
|
return pruneEmpty({ message: message2, thread: thread2 });
|
|
@@ -2492,7 +3133,7 @@ async function handleMessageGet(input) {
|
|
|
2492
3133
|
};
|
|
2493
3134
|
const msg = await fetchMessage(client, { ref, includeReactions });
|
|
2494
3135
|
const thread = await getThreadSummary(client, { channelId, msg });
|
|
2495
|
-
const downloadedPaths = await
|
|
3136
|
+
const downloadedPaths = await downloadMessageFiles({ auth, messages: [msg] });
|
|
2496
3137
|
const maxBodyChars = Number.parseInt(input.options.maxBodyChars, 10);
|
|
2497
3138
|
const message = toCompactMessage(msg, { maxBodyChars, includeReactions, downloadedPaths });
|
|
2498
3139
|
return pruneEmpty({ message, thread });
|
|
@@ -2505,9 +3146,15 @@ async function handleMessageList(input) {
|
|
|
2505
3146
|
return input.ctx.withAutoRefresh({
|
|
2506
3147
|
workspaceUrl: target.kind === "url" ? target.ref.workspace_url : workspaceUrl,
|
|
2507
3148
|
work: async () => {
|
|
3149
|
+
const withReactions = parseReactionFilters(input.options.withReaction);
|
|
3150
|
+
const withoutReactions = parseReactionFilters(input.options.withoutReaction);
|
|
3151
|
+
const hasReactionFilters = withReactions.length > 0 || withoutReactions.length > 0;
|
|
2508
3152
|
if (target.kind === "url") {
|
|
3153
|
+
if (hasReactionFilters) {
|
|
3154
|
+
throw new Error("Reaction filters are only supported for channel history mode (not message URL thread mode)");
|
|
3155
|
+
}
|
|
2509
3156
|
const { ref } = target;
|
|
2510
|
-
|
|
3157
|
+
warnOnTruncatedSlackUrl(ref);
|
|
2511
3158
|
const { client: client2, auth: auth2 } = await input.ctx.getClientForWorkspace(ref.workspace_url);
|
|
2512
3159
|
const includeReactions2 = Boolean(input.options.includeReactions);
|
|
2513
3160
|
const msg = await fetchMessage(client2, { ref, includeReactions: includeReactions2 });
|
|
@@ -2517,7 +3164,7 @@ async function handleMessageList(input) {
|
|
|
2517
3164
|
threadTs: rootTs2,
|
|
2518
3165
|
includeReactions: includeReactions2
|
|
2519
3166
|
});
|
|
2520
|
-
const downloadedPaths2 = await
|
|
3167
|
+
const downloadedPaths2 = await downloadMessageFiles({ auth: auth2, messages: threadMessages2 });
|
|
2521
3168
|
const maxBodyChars2 = Number.parseInt(input.options.maxBodyChars, 10);
|
|
2522
3169
|
return pruneEmpty({
|
|
2523
3170
|
messages: threadMessages2.map((m) => toCompactMessage(m, { maxBodyChars: maxBodyChars2, includeReactions: includeReactions2, downloadedPaths: downloadedPaths2 })).map(toThreadListMessage)
|
|
@@ -2532,7 +3179,31 @@ async function handleMessageList(input) {
|
|
|
2532
3179
|
const threadTs = input.options.threadTs?.trim();
|
|
2533
3180
|
const ts = input.options.ts?.trim();
|
|
2534
3181
|
if (!threadTs && !ts) {
|
|
2535
|
-
|
|
3182
|
+
const includeReactions2 = Boolean(input.options.includeReactions);
|
|
3183
|
+
const limit = parseLimit(input.options.limit);
|
|
3184
|
+
const oldest = requireOldestWhenReactionFiltersUsed({
|
|
3185
|
+
oldest: input.options.oldest,
|
|
3186
|
+
withReactions,
|
|
3187
|
+
withoutReactions
|
|
3188
|
+
});
|
|
3189
|
+
const channelMessages = await fetchChannelHistory(client, {
|
|
3190
|
+
channelId,
|
|
3191
|
+
limit,
|
|
3192
|
+
latest: input.options.latest?.trim(),
|
|
3193
|
+
oldest,
|
|
3194
|
+
includeReactions: includeReactions2 || hasReactionFilters,
|
|
3195
|
+
withReactions,
|
|
3196
|
+
withoutReactions
|
|
3197
|
+
});
|
|
3198
|
+
const downloadedPaths2 = await downloadMessageFiles({ auth, messages: channelMessages });
|
|
3199
|
+
const maxBodyChars2 = Number.parseInt(input.options.maxBodyChars, 10);
|
|
3200
|
+
return pruneEmpty({
|
|
3201
|
+
channel_id: channelId,
|
|
3202
|
+
messages: channelMessages.map((m) => toCompactMessage(m, { maxBodyChars: maxBodyChars2, includeReactions: includeReactions2, downloadedPaths: downloadedPaths2 }))
|
|
3203
|
+
});
|
|
3204
|
+
}
|
|
3205
|
+
if (hasReactionFilters) {
|
|
3206
|
+
throw new Error("Reaction filters are only supported for channel history mode (without --thread-ts/--ts)");
|
|
2536
3207
|
}
|
|
2537
3208
|
const rootTs = threadTs ?? await (async () => {
|
|
2538
3209
|
const ref = {
|
|
@@ -2551,7 +3222,7 @@ async function handleMessageList(input) {
|
|
|
2551
3222
|
threadTs: rootTs,
|
|
2552
3223
|
includeReactions
|
|
2553
3224
|
});
|
|
2554
|
-
const downloadedPaths = await
|
|
3225
|
+
const downloadedPaths = await downloadMessageFiles({ auth, messages: threadMessages });
|
|
2555
3226
|
const maxBodyChars = Number.parseInt(input.options.maxBodyChars, 10);
|
|
2556
3227
|
return pruneEmpty({
|
|
2557
3228
|
messages: threadMessages.map((m) => toCompactMessage(m, { maxBodyChars, includeReactions, downloadedPaths })).map(toThreadListMessage)
|
|
@@ -2563,7 +3234,7 @@ async function sendMessage(input) {
|
|
|
2563
3234
|
const target = parseMsgTarget(String(input.targetInput));
|
|
2564
3235
|
if (target.kind === "url") {
|
|
2565
3236
|
const { ref } = target;
|
|
2566
|
-
|
|
3237
|
+
warnOnTruncatedSlackUrl(ref);
|
|
2567
3238
|
await input.ctx.withAutoRefresh({
|
|
2568
3239
|
workspaceUrl: ref.workspace_url,
|
|
2569
3240
|
work: async () => {
|
|
@@ -2598,6 +3269,70 @@ async function sendMessage(input) {
|
|
|
2598
3269
|
});
|
|
2599
3270
|
return { ok: true };
|
|
2600
3271
|
}
|
|
3272
|
+
async function editMessage(input) {
|
|
3273
|
+
const target = parseMsgTarget(String(input.targetInput));
|
|
3274
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(input.options.workspace);
|
|
3275
|
+
await input.ctx.withAutoRefresh({
|
|
3276
|
+
workspaceUrl: target.kind === "url" ? target.ref.workspace_url : workspaceUrl,
|
|
3277
|
+
work: async () => {
|
|
3278
|
+
if (target.kind === "url") {
|
|
3279
|
+
const { ref } = target;
|
|
3280
|
+
warnOnTruncatedSlackUrl(ref);
|
|
3281
|
+
const { client: client2 } = await input.ctx.getClientForWorkspace(ref.workspace_url);
|
|
3282
|
+
await client2.api("chat.update", {
|
|
3283
|
+
channel: ref.channel_id,
|
|
3284
|
+
ts: ref.message_ts,
|
|
3285
|
+
text: input.text
|
|
3286
|
+
});
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
const ts = requireMessageTs(input.options.ts);
|
|
3290
|
+
await input.ctx.assertWorkspaceSpecifiedForChannelNames({
|
|
3291
|
+
workspaceUrl,
|
|
3292
|
+
channels: [target.channel]
|
|
3293
|
+
});
|
|
3294
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
3295
|
+
const channelId = await resolveChannelId(client, target.channel);
|
|
3296
|
+
await client.api("chat.update", {
|
|
3297
|
+
channel: channelId,
|
|
3298
|
+
ts,
|
|
3299
|
+
text: input.text
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
});
|
|
3303
|
+
return { ok: true };
|
|
3304
|
+
}
|
|
3305
|
+
async function deleteMessage(input) {
|
|
3306
|
+
const target = parseMsgTarget(String(input.targetInput));
|
|
3307
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(input.options.workspace);
|
|
3308
|
+
await input.ctx.withAutoRefresh({
|
|
3309
|
+
workspaceUrl: target.kind === "url" ? target.ref.workspace_url : workspaceUrl,
|
|
3310
|
+
work: async () => {
|
|
3311
|
+
if (target.kind === "url") {
|
|
3312
|
+
const { ref } = target;
|
|
3313
|
+
warnOnTruncatedSlackUrl(ref);
|
|
3314
|
+
const { client: client2 } = await input.ctx.getClientForWorkspace(ref.workspace_url);
|
|
3315
|
+
await client2.api("chat.delete", {
|
|
3316
|
+
channel: ref.channel_id,
|
|
3317
|
+
ts: ref.message_ts
|
|
3318
|
+
});
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
const ts = requireMessageTs(input.options.ts);
|
|
3322
|
+
await input.ctx.assertWorkspaceSpecifiedForChannelNames({
|
|
3323
|
+
workspaceUrl,
|
|
3324
|
+
channels: [target.channel]
|
|
3325
|
+
});
|
|
3326
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
3327
|
+
const channelId = await resolveChannelId(client, target.channel);
|
|
3328
|
+
await client.api("chat.delete", {
|
|
3329
|
+
channel: channelId,
|
|
3330
|
+
ts
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
});
|
|
3334
|
+
return { ok: true };
|
|
3335
|
+
}
|
|
2601
3336
|
async function reactOnTarget(input) {
|
|
2602
3337
|
const target = parseMsgTarget(input.targetInput);
|
|
2603
3338
|
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(input.options?.workspace);
|
|
@@ -2606,7 +3341,7 @@ async function reactOnTarget(input) {
|
|
|
2606
3341
|
work: async () => {
|
|
2607
3342
|
if (target.kind === "url") {
|
|
2608
3343
|
const { ref } = target;
|
|
2609
|
-
|
|
3344
|
+
warnOnTruncatedSlackUrl(ref);
|
|
2610
3345
|
const { client: client2 } = await input.ctx.getClientForWorkspace(ref.workspace_url);
|
|
2611
3346
|
const name2 = normalizeSlackReactionName(input.emoji);
|
|
2612
3347
|
await client2.api(`reactions.${input.action}`, {
|
|
@@ -2616,10 +3351,7 @@ async function reactOnTarget(input) {
|
|
|
2616
3351
|
});
|
|
2617
3352
|
return;
|
|
2618
3353
|
}
|
|
2619
|
-
const ts = input.options?.ts
|
|
2620
|
-
if (!ts) {
|
|
2621
|
-
throw new Error('When targeting a channel, you must pass --ts "<seconds>.<micros>"');
|
|
2622
|
-
}
|
|
3354
|
+
const ts = requireMessageTs(input.options?.ts);
|
|
2623
3355
|
await input.ctx.assertWorkspaceSpecifiedForChannelNames({
|
|
2624
3356
|
workspaceUrl,
|
|
2625
3357
|
channels: [target.channel]
|
|
@@ -2638,9 +3370,12 @@ async function reactOnTarget(input) {
|
|
|
2638
3370
|
}
|
|
2639
3371
|
|
|
2640
3372
|
// src/cli/message-command.ts
|
|
3373
|
+
function collectOptionValue(value, previous = []) {
|
|
3374
|
+
return [...previous, value];
|
|
3375
|
+
}
|
|
2641
3376
|
function registerMessageCommand(input) {
|
|
2642
3377
|
const messageCmd = input.program.command("message").description("Read/write Slack messages (token-efficient JSON)");
|
|
2643
|
-
messageCmd.command("get", { isDefault: true }).description("Fetch a single Slack message (with thread summary if any)").argument("<target>", "Slack message URL, #channel, or channel ID").option("--workspace <url>", "Workspace URL
|
|
3378
|
+
messageCmd.command("get", { isDefault: true }).description("Fetch a single Slack message (with thread summary if any)").argument("<target>", "Slack message URL, #channel, or channel ID").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--ts <ts>", "Message ts (required when using #channel/channel id)").option("--thread-ts <ts>", "Thread root ts hint (useful for thread permalinks)").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) => {
|
|
2644
3379
|
const [targetInput, options] = args;
|
|
2645
3380
|
try {
|
|
2646
3381
|
const payload = await handleMessageGet({ ctx: input.ctx, targetInput, options });
|
|
@@ -2650,7 +3385,7 @@ function registerMessageCommand(input) {
|
|
|
2650
3385
|
process.exitCode = 1;
|
|
2651
3386
|
}
|
|
2652
3387
|
});
|
|
2653
|
-
messageCmd.command("list").description("
|
|
3388
|
+
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 selector (full URL or unique substring; needed when using #channel/channel id across 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("--with-reaction <emoji>", "Only include messages with this reaction (repeatable; channel history mode; requires --oldest)", collectOptionValue, []).option("--without-reaction <emoji>", "Only include messages without this reaction (repeatable; channel history mode; requires --oldest)", collectOptionValue, []).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) => {
|
|
2654
3389
|
const [targetInput, options] = args;
|
|
2655
3390
|
try {
|
|
2656
3391
|
const payload = await handleMessageList({ ctx: input.ctx, targetInput, options });
|
|
@@ -2660,7 +3395,36 @@ function registerMessageCommand(input) {
|
|
|
2660
3395
|
process.exitCode = 1;
|
|
2661
3396
|
}
|
|
2662
3397
|
});
|
|
2663
|
-
messageCmd.command("
|
|
3398
|
+
messageCmd.command("edit").description("Edit a message").argument("<target>", "Slack message URL, #channel, or channel ID").argument("<text>", "New message text").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--ts <ts>", "Message ts (required when using #channel/channel id)").action(async (...args) => {
|
|
3399
|
+
const [targetInput, text, options] = args;
|
|
3400
|
+
try {
|
|
3401
|
+
const payload = await editMessage({
|
|
3402
|
+
ctx: input.ctx,
|
|
3403
|
+
targetInput,
|
|
3404
|
+
text,
|
|
3405
|
+
options
|
|
3406
|
+
});
|
|
3407
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3408
|
+
} catch (err) {
|
|
3409
|
+
console.error(input.ctx.errorMessage(err));
|
|
3410
|
+
process.exitCode = 1;
|
|
3411
|
+
}
|
|
3412
|
+
});
|
|
3413
|
+
messageCmd.command("delete").description("Delete a message").argument("<target>", "Slack message URL, #channel, or channel ID").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--ts <ts>", "Message ts (required when using #channel/channel id)").action(async (...args) => {
|
|
3414
|
+
const [targetInput, options] = args;
|
|
3415
|
+
try {
|
|
3416
|
+
const payload = await deleteMessage({
|
|
3417
|
+
ctx: input.ctx,
|
|
3418
|
+
targetInput,
|
|
3419
|
+
options
|
|
3420
|
+
});
|
|
3421
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3422
|
+
} catch (err) {
|
|
3423
|
+
console.error(input.ctx.errorMessage(err));
|
|
3424
|
+
process.exitCode = 1;
|
|
3425
|
+
}
|
|
3426
|
+
});
|
|
3427
|
+
messageCmd.command("send").description("Send a message (optionally into a thread)").argument("<target>", "Slack message URL, #name/name, or channel id").argument("<text>", "Message text to post").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--thread-ts <ts>", "Thread root ts to post into (optional)").action(async (...args) => {
|
|
2664
3428
|
const [targetInput, text, options] = args;
|
|
2665
3429
|
try {
|
|
2666
3430
|
const payload = await sendMessage({
|
|
@@ -2676,7 +3440,7 @@ function registerMessageCommand(input) {
|
|
|
2676
3440
|
}
|
|
2677
3441
|
});
|
|
2678
3442
|
const reactCmd = messageCmd.command("react").description("Add or remove reactions");
|
|
2679
|
-
reactCmd.command("add").description("Add a reaction to a message").argument("<target>", "Slack message URL, #channel, or channel ID").argument("<emoji>", "Emoji to react with (:rocket:, rocket, or \uD83D\uDE80)").option("--workspace <url>", "Workspace URL
|
|
3443
|
+
reactCmd.command("add").description("Add a reaction to a message").argument("<target>", "Slack message URL, #channel, or channel ID").argument("<emoji>", "Emoji to react with (:rocket:, rocket, or \uD83D\uDE80)").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--ts <ts>", "Message ts (required when using #channel/channel id)").action(async (...args) => {
|
|
2680
3444
|
const [targetInput, emoji2, options] = args;
|
|
2681
3445
|
try {
|
|
2682
3446
|
const payload = await reactOnTarget({
|
|
@@ -2692,7 +3456,7 @@ function registerMessageCommand(input) {
|
|
|
2692
3456
|
process.exitCode = 1;
|
|
2693
3457
|
}
|
|
2694
3458
|
});
|
|
2695
|
-
reactCmd.command("remove").description("Remove a reaction from a message").argument("<target>", "Slack message URL, #channel, or channel ID").argument("<emoji>", "Emoji to remove (:rocket:, rocket, or \uD83D\uDE80)").option("--workspace <url>", "Workspace URL
|
|
3459
|
+
reactCmd.command("remove").description("Remove a reaction from a message").argument("<target>", "Slack message URL, #channel, or channel ID").argument("<emoji>", "Emoji to remove (:rocket:, rocket, or \uD83D\uDE80)").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--ts <ts>", "Message ts (required when using #channel/channel id)").action(async (...args) => {
|
|
2696
3460
|
const [targetInput, emoji2, options] = args;
|
|
2697
3461
|
try {
|
|
2698
3462
|
const payload = await reactOnTarget({
|
|
@@ -2710,20 +3474,6 @@ function registerMessageCommand(input) {
|
|
|
2710
3474
|
});
|
|
2711
3475
|
}
|
|
2712
3476
|
|
|
2713
|
-
// src/slack/search-guards.ts
|
|
2714
|
-
function isRecord10(value) {
|
|
2715
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2716
|
-
}
|
|
2717
|
-
function asArray4(value) {
|
|
2718
|
-
return Array.isArray(value) ? value : [];
|
|
2719
|
-
}
|
|
2720
|
-
function getString5(value) {
|
|
2721
|
-
return typeof value === "string" ? value : undefined;
|
|
2722
|
-
}
|
|
2723
|
-
function getNumber3(value) {
|
|
2724
|
-
return typeof value === "number" ? value : undefined;
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
3477
|
// src/slack/search-query.ts
|
|
2728
3478
|
async function buildSlackSearchQuery(client, input) {
|
|
2729
3479
|
const parts = [];
|
|
@@ -2776,8 +3526,8 @@ async function userTokenForSearch(client, user) {
|
|
|
2776
3526
|
if (/^U[A-Z0-9]{8,}$/.test(trimmed)) {
|
|
2777
3527
|
try {
|
|
2778
3528
|
const info = await client.api("users.info", { user: trimmed });
|
|
2779
|
-
const infoUser =
|
|
2780
|
-
const name =
|
|
3529
|
+
const infoUser = isRecord5(info) ? info.user : null;
|
|
3530
|
+
const name = isRecord5(infoUser) ? (getString(infoUser.name) ?? "").trim() : "";
|
|
2781
3531
|
return name ? `from:@${name}` : null;
|
|
2782
3532
|
} catch {
|
|
2783
3533
|
return null;
|
|
@@ -2798,8 +3548,8 @@ async function channelTokenForSearch(client, channel) {
|
|
|
2798
3548
|
const info = await client.api("conversations.info", {
|
|
2799
3549
|
channel: normalized.value
|
|
2800
3550
|
});
|
|
2801
|
-
const channelInfo =
|
|
2802
|
-
const name =
|
|
3551
|
+
const channelInfo = isRecord5(info) ? info.channel : null;
|
|
3552
|
+
const name = isRecord5(channelInfo) ? (getString(channelInfo.name) ?? "").trim() : "";
|
|
2803
3553
|
if (name) {
|
|
2804
3554
|
return `in:#${name}`;
|
|
2805
3555
|
}
|
|
@@ -2818,19 +3568,19 @@ async function resolveUserId(client, input) {
|
|
|
2818
3568
|
let cursor;
|
|
2819
3569
|
for (;; ) {
|
|
2820
3570
|
const resp = await client.api("users.list", { limit: 200, cursor });
|
|
2821
|
-
const members =
|
|
3571
|
+
const members = isRecord5(resp) ? asArray(resp.members).filter(isRecord5) : [];
|
|
2822
3572
|
const found = members.find((m) => {
|
|
2823
|
-
const mName =
|
|
2824
|
-
const profile =
|
|
2825
|
-
const display = profile ?
|
|
3573
|
+
const mName = getString(m.name);
|
|
3574
|
+
const profile = isRecord5(m.profile) ? m.profile : null;
|
|
3575
|
+
const display = profile ? getString(profile.display_name) : undefined;
|
|
2826
3576
|
return mName === name || display === name;
|
|
2827
3577
|
});
|
|
2828
|
-
const foundId = found ?
|
|
3578
|
+
const foundId = found ? getString(found.id) : undefined;
|
|
2829
3579
|
if (foundId) {
|
|
2830
3580
|
return foundId;
|
|
2831
3581
|
}
|
|
2832
|
-
const meta =
|
|
2833
|
-
const next =
|
|
3582
|
+
const meta = isRecord5(resp) ? resp.response_metadata : null;
|
|
3583
|
+
const next = isRecord5(meta) ? getString(meta.next_cursor) : undefined;
|
|
2834
3584
|
if (!next) {
|
|
2835
3585
|
break;
|
|
2836
3586
|
}
|
|
@@ -2854,11 +3604,11 @@ async function searchMessagesRaw(client, input) {
|
|
|
2854
3604
|
sort: "timestamp",
|
|
2855
3605
|
sort_dir: "desc"
|
|
2856
3606
|
});
|
|
2857
|
-
const messages =
|
|
2858
|
-
const matches =
|
|
3607
|
+
const messages = isRecord5(resp) ? resp.messages : null;
|
|
3608
|
+
const matches = isRecord5(messages) ? asArray(messages.matches).filter(isRecord5) : [];
|
|
2859
3609
|
out.push(...matches);
|
|
2860
|
-
const paging =
|
|
2861
|
-
const totalPages = Number(
|
|
3610
|
+
const paging = isRecord5(messages) ? messages.paging ?? messages.pagination : null;
|
|
3611
|
+
const totalPages = Number(isRecord5(paging) ? paging.pages ?? 1 : 1);
|
|
2862
3612
|
if (Number.isFinite(totalPages) && totalPages > 0) {
|
|
2863
3613
|
pages = totalPages;
|
|
2864
3614
|
}
|
|
@@ -2889,11 +3639,11 @@ async function searchFilesRaw(client, input) {
|
|
|
2889
3639
|
sort: "timestamp",
|
|
2890
3640
|
sort_dir: "desc"
|
|
2891
3641
|
});
|
|
2892
|
-
const files =
|
|
2893
|
-
const matches =
|
|
3642
|
+
const files = isRecord5(resp) ? resp.files : null;
|
|
3643
|
+
const matches = isRecord5(files) ? asArray(files.matches).filter(isRecord5) : [];
|
|
2894
3644
|
out.push(...matches);
|
|
2895
|
-
const paging =
|
|
2896
|
-
const totalPages = Number(
|
|
3645
|
+
const paging = isRecord5(files) ? files.paging ?? files.pagination : null;
|
|
3646
|
+
const totalPages = Number(isRecord5(paging) ? paging.pages ?? 1 : 1);
|
|
2897
3647
|
if (Number.isFinite(totalPages) && totalPages > 0) {
|
|
2898
3648
|
pages = totalPages;
|
|
2899
3649
|
}
|
|
@@ -2912,7 +3662,7 @@ async function searchFilesRaw(client, input) {
|
|
|
2912
3662
|
}
|
|
2913
3663
|
|
|
2914
3664
|
// src/slack/search-file-ext.ts
|
|
2915
|
-
function
|
|
3665
|
+
function inferExt(file) {
|
|
2916
3666
|
const mt = (file.mimetype || "").toLowerCase();
|
|
2917
3667
|
const ft = (file.filetype || "").toLowerCase();
|
|
2918
3668
|
if (mt === "image/png" || ft === "png") {
|
|
@@ -2950,22 +3700,22 @@ async function searchFilesViaSearchApi(client, input) {
|
|
|
2950
3700
|
const downloadsDir = await ensureDownloadsDir();
|
|
2951
3701
|
const out = [];
|
|
2952
3702
|
for (const f of matches) {
|
|
2953
|
-
const mode =
|
|
2954
|
-
const mimetype =
|
|
3703
|
+
const mode = getString(f.mode);
|
|
3704
|
+
const mimetype = getString(f.mimetype);
|
|
2955
3705
|
if (!passesFileContentTypeFilter({ mode, mimetype }, input.contentType)) {
|
|
2956
3706
|
continue;
|
|
2957
3707
|
}
|
|
2958
|
-
const url =
|
|
3708
|
+
const url = getString(f.url_private_download) ?? getString(f.url_private);
|
|
2959
3709
|
if (!url) {
|
|
2960
3710
|
continue;
|
|
2961
3711
|
}
|
|
2962
|
-
const ext =
|
|
3712
|
+
const ext = inferExt({
|
|
2963
3713
|
mimetype,
|
|
2964
|
-
filetype:
|
|
2965
|
-
name:
|
|
2966
|
-
title:
|
|
3714
|
+
filetype: getString(f.filetype),
|
|
3715
|
+
name: getString(f.name),
|
|
3716
|
+
title: getString(f.title)
|
|
2967
3717
|
});
|
|
2968
|
-
const id =
|
|
3718
|
+
const id = getString(f.id);
|
|
2969
3719
|
if (!id) {
|
|
2970
3720
|
continue;
|
|
2971
3721
|
}
|
|
@@ -2975,7 +3725,7 @@ async function searchFilesViaSearchApi(client, input) {
|
|
|
2975
3725
|
destDir: downloadsDir,
|
|
2976
3726
|
preferredName: `${id}${ext ? `.${ext}` : ""}`
|
|
2977
3727
|
});
|
|
2978
|
-
const title = (
|
|
3728
|
+
const title = (getString(f.title) || getString(f.name) || "").trim();
|
|
2979
3729
|
out.push({
|
|
2980
3730
|
title: title || undefined,
|
|
2981
3731
|
mimetype,
|
|
@@ -3007,31 +3757,31 @@ async function searchFilesInChannelsFallback(client, input) {
|
|
|
3007
3757
|
count: 100,
|
|
3008
3758
|
page
|
|
3009
3759
|
});
|
|
3010
|
-
const files =
|
|
3760
|
+
const files = isRecord5(resp) ? asArray(resp.files).filter(isRecord5) : [];
|
|
3011
3761
|
if (files.length === 0) {
|
|
3012
3762
|
break;
|
|
3013
3763
|
}
|
|
3014
3764
|
for (const f of files) {
|
|
3015
|
-
const mode =
|
|
3016
|
-
const mimetype =
|
|
3765
|
+
const mode = getString(f.mode);
|
|
3766
|
+
const mimetype = getString(f.mimetype);
|
|
3017
3767
|
if (!passesFileContentTypeFilter({ mode, mimetype }, input.contentType)) {
|
|
3018
3768
|
continue;
|
|
3019
3769
|
}
|
|
3020
|
-
const title = (
|
|
3770
|
+
const title = (getString(f.title) || getString(f.name) || "").trim();
|
|
3021
3771
|
if (queryLower && !title.toLowerCase().includes(queryLower)) {
|
|
3022
3772
|
continue;
|
|
3023
3773
|
}
|
|
3024
|
-
const url =
|
|
3774
|
+
const url = getString(f.url_private_download) ?? getString(f.url_private);
|
|
3025
3775
|
if (!url) {
|
|
3026
3776
|
continue;
|
|
3027
3777
|
}
|
|
3028
|
-
const ext =
|
|
3778
|
+
const ext = inferExt({
|
|
3029
3779
|
mimetype,
|
|
3030
|
-
filetype:
|
|
3031
|
-
name:
|
|
3032
|
-
title:
|
|
3780
|
+
filetype: getString(f.filetype),
|
|
3781
|
+
name: getString(f.name),
|
|
3782
|
+
title: getString(f.title)
|
|
3033
3783
|
});
|
|
3034
|
-
const id =
|
|
3784
|
+
const id = getString(f.id);
|
|
3035
3785
|
if (!id) {
|
|
3036
3786
|
continue;
|
|
3037
3787
|
}
|
|
@@ -3051,8 +3801,8 @@ async function searchFilesInChannelsFallback(client, input) {
|
|
|
3051
3801
|
return out;
|
|
3052
3802
|
}
|
|
3053
3803
|
}
|
|
3054
|
-
const paging =
|
|
3055
|
-
const pages = Number(
|
|
3804
|
+
const paging = isRecord5(resp) ? isRecord5(resp.paging) ? resp.paging : resp.pagination : null;
|
|
3805
|
+
const pages = Number(isRecord5(paging) ? paging.pages ?? paging.page_count : undefined);
|
|
3056
3806
|
if (Number.isFinite(pages) && page >= pages) {
|
|
3057
3807
|
break;
|
|
3058
3808
|
}
|
|
@@ -3088,19 +3838,19 @@ async function searchMessagesViaSearchApi(client, input) {
|
|
|
3088
3838
|
}
|
|
3089
3839
|
const messageRefs = [];
|
|
3090
3840
|
for (const m of matches) {
|
|
3091
|
-
const ts =
|
|
3841
|
+
const ts = getString(m.ts)?.trim() ?? "";
|
|
3092
3842
|
if (!ts) {
|
|
3093
3843
|
continue;
|
|
3094
3844
|
}
|
|
3095
|
-
const channelValue =
|
|
3096
|
-
const channelId = channelValue &&
|
|
3845
|
+
const channelValue = isRecord5(m.channel) ? m.channel : null;
|
|
3846
|
+
const channelId = channelValue && getString(channelValue.id) ? getString(channelValue.id) : channelValue && getString(channelValue.name) ? await resolveChannelId(client, `#${getString(channelValue.name)}`) : "";
|
|
3097
3847
|
if (!channelId) {
|
|
3098
3848
|
continue;
|
|
3099
3849
|
}
|
|
3100
3850
|
messageRefs.push({
|
|
3101
3851
|
channel_id: channelId,
|
|
3102
3852
|
message_ts: ts,
|
|
3103
|
-
permalink:
|
|
3853
|
+
permalink: getString(m.permalink)
|
|
3104
3854
|
});
|
|
3105
3855
|
if (messageRefs.length >= input.limit) {
|
|
3106
3856
|
break;
|
|
@@ -3170,7 +3920,7 @@ async function searchMessagesInChannelsFallback(client, input) {
|
|
|
3170
3920
|
limit: 200,
|
|
3171
3921
|
latest: cursorLatest
|
|
3172
3922
|
});
|
|
3173
|
-
const messages =
|
|
3923
|
+
const messages = isRecord5(resp) ? asArray(resp.messages).filter(isRecord5) : [];
|
|
3174
3924
|
if (messages.length === 0) {
|
|
3175
3925
|
break;
|
|
3176
3926
|
}
|
|
@@ -3217,7 +3967,7 @@ async function searchMessagesInChannelsFallback(client, input) {
|
|
|
3217
3967
|
break;
|
|
3218
3968
|
}
|
|
3219
3969
|
const last = messages.at(-1);
|
|
3220
|
-
cursorLatest = last ?
|
|
3970
|
+
cursorLatest = last ? getString(last.ts) : undefined;
|
|
3221
3971
|
if (!cursorLatest) {
|
|
3222
3972
|
break;
|
|
3223
3973
|
}
|
|
@@ -3260,7 +4010,7 @@ async function downloadFilesForMessage(input) {
|
|
|
3260
4010
|
if (!url) {
|
|
3261
4011
|
continue;
|
|
3262
4012
|
}
|
|
3263
|
-
const ext =
|
|
4013
|
+
const ext = inferExt(f);
|
|
3264
4014
|
const path = await downloadSlackFile({
|
|
3265
4015
|
auth: input.auth,
|
|
3266
4016
|
url,
|
|
@@ -3271,15 +4021,15 @@ async function downloadFilesForMessage(input) {
|
|
|
3271
4021
|
}
|
|
3272
4022
|
}
|
|
3273
4023
|
function messageSummaryFromApiMessage(channelId, msg) {
|
|
3274
|
-
const text =
|
|
3275
|
-
const files =
|
|
4024
|
+
const text = getString(msg.text) ?? "";
|
|
4025
|
+
const files = asArray(msg.files).map((f) => toSlackFileSummary2(f)).filter((f) => f !== null);
|
|
3276
4026
|
return {
|
|
3277
4027
|
channel_id: channelId,
|
|
3278
|
-
ts:
|
|
3279
|
-
thread_ts:
|
|
3280
|
-
reply_count:
|
|
3281
|
-
user:
|
|
3282
|
-
bot_id:
|
|
4028
|
+
ts: getString(msg.ts) ?? "",
|
|
4029
|
+
thread_ts: getString(msg.thread_ts),
|
|
4030
|
+
reply_count: getNumber(msg.reply_count),
|
|
4031
|
+
user: getString(msg.user),
|
|
4032
|
+
bot_id: getString(msg.bot_id),
|
|
3283
4033
|
text,
|
|
3284
4034
|
markdown: slackMrkdwnToMarkdown(text),
|
|
3285
4035
|
blocks: Array.isArray(msg.blocks) ? msg.blocks : undefined,
|
|
@@ -3289,24 +4039,24 @@ function messageSummaryFromApiMessage(channelId, msg) {
|
|
|
3289
4039
|
};
|
|
3290
4040
|
}
|
|
3291
4041
|
function toSlackFileSummary2(value) {
|
|
3292
|
-
if (!
|
|
4042
|
+
if (!isRecord5(value)) {
|
|
3293
4043
|
return null;
|
|
3294
4044
|
}
|
|
3295
|
-
const id =
|
|
4045
|
+
const id = getString(value.id);
|
|
3296
4046
|
if (!id) {
|
|
3297
4047
|
return null;
|
|
3298
4048
|
}
|
|
3299
4049
|
return {
|
|
3300
4050
|
id,
|
|
3301
|
-
name:
|
|
3302
|
-
title:
|
|
3303
|
-
mimetype:
|
|
3304
|
-
filetype:
|
|
3305
|
-
mode:
|
|
3306
|
-
permalink:
|
|
3307
|
-
url_private:
|
|
3308
|
-
url_private_download:
|
|
3309
|
-
size:
|
|
4051
|
+
name: getString(value.name),
|
|
4052
|
+
title: getString(value.title),
|
|
4053
|
+
mimetype: getString(value.mimetype),
|
|
4054
|
+
filetype: getString(value.filetype),
|
|
4055
|
+
mode: getString(value.mode),
|
|
4056
|
+
permalink: getString(value.permalink),
|
|
4057
|
+
url_private: getString(value.url_private),
|
|
4058
|
+
url_private_download: getString(value.url_private_download),
|
|
4059
|
+
size: getNumber(value.size)
|
|
3310
4060
|
};
|
|
3311
4061
|
}
|
|
3312
4062
|
|
|
@@ -3383,7 +4133,7 @@ async function searchSlack(input) {
|
|
|
3383
4133
|
|
|
3384
4134
|
// src/cli/search-command.ts
|
|
3385
4135
|
function addSearchOptions(cmd) {
|
|
3386
|
-
return cmd.option("--workspace <url>", "Workspace URL
|
|
4136
|
+
return cmd.option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when searching across multiple workspaces)").option("--channel <channel...>", "Channel filter (#name, name, or id). Repeatable.").option("--user <user>", "User filter (@name, name, or user id U...)").option("--after <date>", "Only results after YYYY-MM-DD").option("--before <date>", "Only results before YYYY-MM-DD").option("--content-type <type>", "Filter content type: any|text|image|snippet|file (default any)").option("--limit <n>", "Max results (default 20)", "20").option("--max-content-chars <n>", "Max message content characters (default 4000, -1 for unlimited)", "4000");
|
|
3387
4137
|
}
|
|
3388
4138
|
async function runSearch(input) {
|
|
3389
4139
|
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(input.options.workspace);
|
|
@@ -3435,13 +4185,13 @@ function registerSearchCommand(input) {
|
|
|
3435
4185
|
|
|
3436
4186
|
// src/lib/update.ts
|
|
3437
4187
|
import { createHash } from "node:crypto";
|
|
3438
|
-
import { chmod, copyFile, mkdir as mkdir5, readFile as
|
|
4188
|
+
import { chmod, copyFile, mkdir as mkdir5, readFile as readFile5, rename, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
|
|
3439
4189
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
3440
|
-
import { join as
|
|
4190
|
+
import { join as join10 } from "node:path";
|
|
3441
4191
|
var REPO = "stablyai/agent-slack";
|
|
3442
4192
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
3443
4193
|
function getCachePath() {
|
|
3444
|
-
return
|
|
4194
|
+
return join10(getAppDir(), "update-check.json");
|
|
3445
4195
|
}
|
|
3446
4196
|
function compareSemver(a, b) {
|
|
3447
4197
|
const pa = a.replace(/^v/, "").split(".").map(Number);
|
|
@@ -3498,24 +4248,24 @@ async function checkForUpdate(force = false) {
|
|
|
3498
4248
|
};
|
|
3499
4249
|
}
|
|
3500
4250
|
function detectPlatformAsset() {
|
|
3501
|
-
const
|
|
4251
|
+
const platform6 = process.platform === "win32" ? "windows" : process.platform;
|
|
3502
4252
|
const archMap = { x64: "x64", arm64: "arm64" };
|
|
3503
4253
|
const arch = archMap[process.arch] ?? process.arch;
|
|
3504
|
-
const ext =
|
|
3505
|
-
return `agent-slack-${
|
|
4254
|
+
const ext = platform6 === "windows" ? ".exe" : "";
|
|
4255
|
+
return `agent-slack-${platform6}-${arch}${ext}`;
|
|
3506
4256
|
}
|
|
3507
4257
|
async function sha256(filePath) {
|
|
3508
|
-
const data = await
|
|
4258
|
+
const data = await readFile5(filePath);
|
|
3509
4259
|
return createHash("sha256").update(data).digest("hex");
|
|
3510
4260
|
}
|
|
3511
4261
|
async function performUpdate(latest) {
|
|
3512
4262
|
const asset = detectPlatformAsset();
|
|
3513
4263
|
const tag = `v${latest}`;
|
|
3514
4264
|
const baseUrl = `https://github.com/${REPO}/releases/download/${tag}`;
|
|
3515
|
-
const tmp =
|
|
4265
|
+
const tmp = join10(tmpdir2(), `agent-slack-update-${Date.now()}`);
|
|
3516
4266
|
await mkdir5(tmp, { recursive: true });
|
|
3517
|
-
const binTmp =
|
|
3518
|
-
const sumsTmp =
|
|
4267
|
+
const binTmp = join10(tmp, asset);
|
|
4268
|
+
const sumsTmp = join10(tmp, "checksums-sha256.txt");
|
|
3519
4269
|
try {
|
|
3520
4270
|
const [binResp, sumsResp] = await Promise.all([
|
|
3521
4271
|
fetch(`${baseUrl}/${asset}`, { signal: AbortSignal.timeout(120000) }),
|
|
@@ -3527,9 +4277,9 @@ async function performUpdate(latest) {
|
|
|
3527
4277
|
if (!sumsResp.ok) {
|
|
3528
4278
|
return { success: false, message: `Failed to download checksums: HTTP ${sumsResp.status}` };
|
|
3529
4279
|
}
|
|
3530
|
-
await
|
|
4280
|
+
await writeFile4(binTmp, Buffer.from(await binResp.arrayBuffer()));
|
|
3531
4281
|
const sumsText = await sumsResp.text();
|
|
3532
|
-
await
|
|
4282
|
+
await writeFile4(sumsTmp, sumsText);
|
|
3533
4283
|
const expected = sumsText.split(`
|
|
3534
4284
|
`).map((line) => line.trim().split(/\s+/)).find((parts) => parts[1] === asset)?.[0];
|
|
3535
4285
|
if (!expected) {
|
|
@@ -3623,9 +4373,9 @@ async function listUsers(client, options) {
|
|
|
3623
4373
|
limit: pageSize,
|
|
3624
4374
|
cursor
|
|
3625
4375
|
});
|
|
3626
|
-
const members =
|
|
4376
|
+
const members = asArray(resp.members).filter(isRecord5);
|
|
3627
4377
|
for (const m of members) {
|
|
3628
|
-
const id =
|
|
4378
|
+
const id = getString(m.id);
|
|
3629
4379
|
if (!id) {
|
|
3630
4380
|
continue;
|
|
3631
4381
|
}
|
|
@@ -3637,8 +4387,8 @@ async function listUsers(client, options) {
|
|
|
3637
4387
|
break;
|
|
3638
4388
|
}
|
|
3639
4389
|
}
|
|
3640
|
-
const meta =
|
|
3641
|
-
const next = meta ?
|
|
4390
|
+
const meta = isRecord5(resp.response_metadata) ? resp.response_metadata : null;
|
|
4391
|
+
const next = meta ? getString(meta.next_cursor) : undefined;
|
|
3642
4392
|
if (!next) {
|
|
3643
4393
|
return { users: out };
|
|
3644
4394
|
}
|
|
@@ -3656,8 +4406,8 @@ async function getUser(client, input) {
|
|
|
3656
4406
|
throw new Error(`Could not resolve user: ${input}`);
|
|
3657
4407
|
}
|
|
3658
4408
|
const resp = await client.api("users.info", { user: userId });
|
|
3659
|
-
const u =
|
|
3660
|
-
if (!u || !
|
|
4409
|
+
const u = isRecord5(resp.user) ? resp.user : null;
|
|
4410
|
+
if (!u || !getString(u.id)) {
|
|
3661
4411
|
throw new Error("users.info returned no user");
|
|
3662
4412
|
}
|
|
3663
4413
|
return toCompactUser(u);
|
|
@@ -3674,16 +4424,16 @@ async function resolveUserId2(client, input) {
|
|
|
3674
4424
|
let cursor;
|
|
3675
4425
|
for (;; ) {
|
|
3676
4426
|
const resp = await client.api("users.list", { limit: 200, cursor });
|
|
3677
|
-
const members =
|
|
3678
|
-
const found = members.find((m) =>
|
|
4427
|
+
const members = asArray(resp.members).filter(isRecord5);
|
|
4428
|
+
const found = members.find((m) => getString(m.name) === handle);
|
|
3679
4429
|
if (found) {
|
|
3680
|
-
const id =
|
|
4430
|
+
const id = getString(found.id);
|
|
3681
4431
|
if (id) {
|
|
3682
4432
|
return id;
|
|
3683
4433
|
}
|
|
3684
4434
|
}
|
|
3685
|
-
const meta =
|
|
3686
|
-
const next = meta ?
|
|
4435
|
+
const meta = isRecord5(resp.response_metadata) ? resp.response_metadata : null;
|
|
4436
|
+
const next = meta ? getString(meta.next_cursor) : undefined;
|
|
3687
4437
|
if (!next) {
|
|
3688
4438
|
break;
|
|
3689
4439
|
}
|
|
@@ -3692,33 +4442,24 @@ async function resolveUserId2(client, input) {
|
|
|
3692
4442
|
return null;
|
|
3693
4443
|
}
|
|
3694
4444
|
function toCompactUser(u) {
|
|
3695
|
-
const profile =
|
|
4445
|
+
const profile = isRecord5(u.profile) ? u.profile : {};
|
|
3696
4446
|
return {
|
|
3697
|
-
id:
|
|
3698
|
-
name:
|
|
3699
|
-
real_name:
|
|
3700
|
-
display_name:
|
|
3701
|
-
email:
|
|
3702
|
-
title:
|
|
3703
|
-
tz:
|
|
4447
|
+
id: getString(u.id) ?? "",
|
|
4448
|
+
name: getString(u.name) ?? undefined,
|
|
4449
|
+
real_name: getString(u.real_name) ?? undefined,
|
|
4450
|
+
display_name: getString(profile.display_name) ?? undefined,
|
|
4451
|
+
email: getString(profile.email) ?? undefined,
|
|
4452
|
+
title: getString(profile.title) ?? undefined,
|
|
4453
|
+
tz: getString(u.tz) ?? undefined,
|
|
3704
4454
|
is_bot: typeof u.is_bot === "boolean" ? u.is_bot : undefined,
|
|
3705
4455
|
deleted: typeof u.deleted === "boolean" ? u.deleted : undefined
|
|
3706
4456
|
};
|
|
3707
4457
|
}
|
|
3708
|
-
function isRecord11(value) {
|
|
3709
|
-
return typeof value === "object" && value !== null;
|
|
3710
|
-
}
|
|
3711
|
-
function asArray5(value) {
|
|
3712
|
-
return Array.isArray(value) ? value : [];
|
|
3713
|
-
}
|
|
3714
|
-
function getString6(value) {
|
|
3715
|
-
return typeof value === "string" ? value : undefined;
|
|
3716
|
-
}
|
|
3717
4458
|
|
|
3718
4459
|
// src/cli/user-command.ts
|
|
3719
4460
|
function registerUserCommand(input) {
|
|
3720
4461
|
const userCmd = input.program.command("user").description("Workspace user directory");
|
|
3721
|
-
userCmd.command("list").description("List users in the workspace").option("--workspace <url>", "Workspace URL
|
|
4462
|
+
userCmd.command("list").description("List users in the workspace").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if you have multiple workspaces)").option("--limit <n>", "Max users (default 200)", "200").option("--cursor <cursor>", "Pagination cursor").option("--include-bots", "Include bot users").action(async (...args) => {
|
|
3722
4463
|
const [options] = args;
|
|
3723
4464
|
try {
|
|
3724
4465
|
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
|
|
@@ -3740,7 +4481,7 @@ function registerUserCommand(input) {
|
|
|
3740
4481
|
process.exitCode = 1;
|
|
3741
4482
|
}
|
|
3742
4483
|
});
|
|
3743
|
-
userCmd.command("get").description("Get a single user by id (U...) or handle (@name)").argument("<user>", "User id (U...) or @handle/handle").option("--workspace <url>", "Workspace URL
|
|
4484
|
+
userCmd.command("get").description("Get a single user by id (U...) or handle (@name)").argument("<user>", "User id (U...) or @handle/handle").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if you have multiple workspaces)").action(async (...args) => {
|
|
3744
4485
|
const [user, options] = args;
|
|
3745
4486
|
try {
|
|
3746
4487
|
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
|
|
@@ -3778,5 +4519,5 @@ if (subcommand && subcommand !== "update") {
|
|
|
3778
4519
|
backgroundUpdateCheck();
|
|
3779
4520
|
}
|
|
3780
4521
|
|
|
3781
|
-
//# debugId=
|
|
4522
|
+
//# debugId=6879354D2CF2337D64756E2164756E21
|
|
3782
4523
|
//# sourceMappingURL=index.js.map
|