agent-slack 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/index.js +990 -28
- package/dist/index.js.map +17 -11
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1819,6 +1819,41 @@ class SlackApiClient {
|
|
|
1819
1819
|
this.web = new WebClient(auth.token);
|
|
1820
1820
|
}
|
|
1821
1821
|
}
|
|
1822
|
+
async apiMultipart(method, params = {}) {
|
|
1823
|
+
if (this.auth.auth_type === "standard") {
|
|
1824
|
+
return this.api(method, params);
|
|
1825
|
+
}
|
|
1826
|
+
if (!this.workspaceUrl) {
|
|
1827
|
+
throw new Error("Browser auth requires workspace URL.");
|
|
1828
|
+
}
|
|
1829
|
+
const auth = this.auth;
|
|
1830
|
+
const url = `${this.workspaceUrl.replace(/\/$/, "")}/api/${method}`;
|
|
1831
|
+
const fd = new FormData;
|
|
1832
|
+
fd.append("token", auth.xoxc_token);
|
|
1833
|
+
for (const [k, v] of Object.entries(params)) {
|
|
1834
|
+
if (v !== undefined) {
|
|
1835
|
+
fd.append(k, typeof v === "object" ? JSON.stringify(v) : String(v));
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
const response = await fetch(url, {
|
|
1839
|
+
method: "POST",
|
|
1840
|
+
headers: {
|
|
1841
|
+
Cookie: `d=${encodeURIComponent(auth.xoxd_cookie)}`,
|
|
1842
|
+
Origin: "https://app.slack.com",
|
|
1843
|
+
"User-Agent": getUserAgent()
|
|
1844
|
+
},
|
|
1845
|
+
body: fd
|
|
1846
|
+
});
|
|
1847
|
+
const data = await response.json().catch(() => ({}));
|
|
1848
|
+
if (!response.ok) {
|
|
1849
|
+
throw new Error(`Slack HTTP ${response.status} calling ${method}`);
|
|
1850
|
+
}
|
|
1851
|
+
if (!isRecord3(data) || data.ok !== true) {
|
|
1852
|
+
const error = isRecord3(data) && typeof data.error === "string" ? data.error : null;
|
|
1853
|
+
throw new Error(error || `Slack API error calling ${method}`);
|
|
1854
|
+
}
|
|
1855
|
+
return data;
|
|
1856
|
+
}
|
|
1822
1857
|
async api(method, params = {}) {
|
|
1823
1858
|
if (this.auth.auth_type === "standard") {
|
|
1824
1859
|
if (!this.web) {
|
|
@@ -2417,6 +2452,14 @@ function registerAuthCommand(input) {
|
|
|
2417
2452
|
import { mkdir as mkdir3, writeFile as writeFile2 } from "node:fs/promises";
|
|
2418
2453
|
import { basename, join as join9, resolve } from "node:path";
|
|
2419
2454
|
import { existsSync as existsSync7 } from "node:fs";
|
|
2455
|
+
class SlackDownloadError extends Error {
|
|
2456
|
+
httpStatus;
|
|
2457
|
+
constructor(message, httpStatus) {
|
|
2458
|
+
super(message);
|
|
2459
|
+
this.httpStatus = httpStatus;
|
|
2460
|
+
this.name = "SlackDownloadError";
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2420
2463
|
async function downloadSlackFile(input) {
|
|
2421
2464
|
const { auth, url, destDir, preferredName, options } = input;
|
|
2422
2465
|
const absDir = resolve(destDir);
|
|
@@ -2435,19 +2478,54 @@ async function downloadSlackFile(input) {
|
|
|
2435
2478
|
headers.Referer = "https://app.slack.com/";
|
|
2436
2479
|
headers["User-Agent"] = getUserAgent();
|
|
2437
2480
|
}
|
|
2438
|
-
|
|
2481
|
+
let resp;
|
|
2482
|
+
try {
|
|
2483
|
+
resp = await fetch(url, { headers });
|
|
2484
|
+
} catch (err) {
|
|
2485
|
+
throw new SlackDownloadError(`Network error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2486
|
+
}
|
|
2439
2487
|
if (!resp.ok) {
|
|
2440
|
-
throw new
|
|
2488
|
+
throw new SlackDownloadError(`Failed to download file (${resp.status})`, resp.status);
|
|
2441
2489
|
}
|
|
2442
2490
|
const contentType = resp.headers.get("content-type") || "";
|
|
2443
2491
|
if (!options?.allowHtml && contentType.includes("text/html")) {
|
|
2444
|
-
|
|
2445
|
-
|
|
2492
|
+
let text;
|
|
2493
|
+
try {
|
|
2494
|
+
text = await resp.text();
|
|
2495
|
+
} catch (err) {
|
|
2496
|
+
throw new SlackDownloadError(`Failed to read download response body: ${err instanceof Error ? err.message : String(err)}`, resp.status);
|
|
2497
|
+
}
|
|
2498
|
+
throw new SlackDownloadError(`Downloaded HTML instead of file (auth likely failed). First bytes: ${JSON.stringify(text.slice(0, 120))}`, resp.status);
|
|
2446
2499
|
}
|
|
2447
|
-
|
|
2500
|
+
let arrayBuffer;
|
|
2501
|
+
try {
|
|
2502
|
+
arrayBuffer = await resp.arrayBuffer();
|
|
2503
|
+
} catch (err) {
|
|
2504
|
+
throw new SlackDownloadError(`Failed to read download response body: ${err instanceof Error ? err.message : String(err)}`, resp.status);
|
|
2505
|
+
}
|
|
2506
|
+
const buf = Buffer.from(arrayBuffer);
|
|
2448
2507
|
await writeFile2(path, buf);
|
|
2449
2508
|
return path;
|
|
2450
2509
|
}
|
|
2510
|
+
async function tryDownloadSlackFile(input) {
|
|
2511
|
+
try {
|
|
2512
|
+
const path = await downloadSlackFile(input);
|
|
2513
|
+
return { ok: true, path };
|
|
2514
|
+
} catch (err) {
|
|
2515
|
+
if (err instanceof SlackDownloadError) {
|
|
2516
|
+
return { ok: false, error: err.message, httpStatus: err.httpStatus };
|
|
2517
|
+
}
|
|
2518
|
+
throw err;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
async function writeDownloadErrorFile(input) {
|
|
2522
|
+
const absDir = resolve(input.destDir);
|
|
2523
|
+
await mkdir3(absDir, { recursive: true });
|
|
2524
|
+
const path = join9(absDir, sanitizeFilename(`${input.fileId}.download-error.txt`));
|
|
2525
|
+
await writeFile2(path, `${input.error}
|
|
2526
|
+
`, "utf8");
|
|
2527
|
+
return path;
|
|
2528
|
+
}
|
|
2451
2529
|
function sanitizeFilename(name) {
|
|
2452
2530
|
return name.replace(/[\\/<>:"|?*]/g, "_");
|
|
2453
2531
|
}
|
|
@@ -3129,14 +3207,16 @@ function toCompactMessage(msg, input) {
|
|
|
3129
3207
|
const content = maxBodyChars >= 0 && rendered.length > maxBodyChars ? `${rendered.slice(0, maxBodyChars)}
|
|
3130
3208
|
…` : rendered;
|
|
3131
3209
|
const files = msg.files?.map((f) => {
|
|
3132
|
-
const
|
|
3133
|
-
if (!
|
|
3210
|
+
const entry = input?.downloadedPaths?.[f.id];
|
|
3211
|
+
if (!entry) {
|
|
3134
3212
|
return null;
|
|
3135
3213
|
}
|
|
3136
|
-
return {
|
|
3214
|
+
return entry.ok ? { name: f.name, mimetype: f.mimetype, mode: f.mode, path: entry.path } : {
|
|
3215
|
+
name: f.name,
|
|
3137
3216
|
mimetype: f.mimetype,
|
|
3138
3217
|
mode: f.mode,
|
|
3139
|
-
path
|
|
3218
|
+
path: entry.path,
|
|
3219
|
+
error: entry.error
|
|
3140
3220
|
};
|
|
3141
3221
|
}).filter((f) => Boolean(f));
|
|
3142
3222
|
return {
|
|
@@ -3743,7 +3823,7 @@ async function downloadCanvasAsMarkdown(input) {
|
|
|
3743
3823
|
});
|
|
3744
3824
|
const html = await readFile6(htmlPath, "utf8");
|
|
3745
3825
|
if (looksLikeAuthPage(html)) {
|
|
3746
|
-
throw new
|
|
3826
|
+
throw new SlackDownloadError("Downloaded auth/login page instead of canvas content (token may be expired)");
|
|
3747
3827
|
}
|
|
3748
3828
|
const markdown = htmlToMarkdown(html).trim();
|
|
3749
3829
|
const safeName = `${input.fileId.replace(/[\\/<>"|?*]/g, "_")}.md`;
|
|
@@ -3764,25 +3844,53 @@ async function downloadMessageFiles(input) {
|
|
|
3764
3844
|
if (!url) {
|
|
3765
3845
|
continue;
|
|
3766
3846
|
}
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3847
|
+
if (isCanvas) {
|
|
3848
|
+
try {
|
|
3849
|
+
const path = await downloadCanvasAsMarkdown({
|
|
3770
3850
|
auth: input.auth,
|
|
3771
3851
|
fileId: file.id,
|
|
3772
3852
|
url,
|
|
3773
3853
|
destDir: downloadsDir
|
|
3774
3854
|
});
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3855
|
+
downloadedPaths[file.id] = { ok: true, path };
|
|
3856
|
+
} catch (err) {
|
|
3857
|
+
if (!(err instanceof SlackDownloadError)) {
|
|
3858
|
+
throw err;
|
|
3859
|
+
}
|
|
3860
|
+
const path = await writeDownloadErrorFile({
|
|
3780
3861
|
destDir: downloadsDir,
|
|
3781
|
-
|
|
3862
|
+
fileId: file.id,
|
|
3863
|
+
error: err.message
|
|
3782
3864
|
});
|
|
3865
|
+
downloadedPaths[file.id] = {
|
|
3866
|
+
ok: false,
|
|
3867
|
+
error: err.message,
|
|
3868
|
+
httpStatus: err.httpStatus,
|
|
3869
|
+
path
|
|
3870
|
+
};
|
|
3871
|
+
console.error(`Warning: skipping file ${file.id}: ${err.message}`);
|
|
3872
|
+
}
|
|
3873
|
+
} else {
|
|
3874
|
+
const ext = inferFileExtension(file);
|
|
3875
|
+
const result = await tryDownloadSlackFile({
|
|
3876
|
+
auth: input.auth,
|
|
3877
|
+
url,
|
|
3878
|
+
destDir: downloadsDir,
|
|
3879
|
+
preferredName: `${file.id}${ext ? `.${ext}` : ""}`
|
|
3880
|
+
});
|
|
3881
|
+
if (!result.ok) {
|
|
3882
|
+
downloadedPaths[file.id] = {
|
|
3883
|
+
...result,
|
|
3884
|
+
path: await writeDownloadErrorFile({
|
|
3885
|
+
destDir: downloadsDir,
|
|
3886
|
+
fileId: file.id,
|
|
3887
|
+
error: result.error
|
|
3888
|
+
})
|
|
3889
|
+
};
|
|
3890
|
+
console.error(`Warning: skipping file ${file.id}: ${result.error}`);
|
|
3891
|
+
} else {
|
|
3892
|
+
downloadedPaths[file.id] = result;
|
|
3783
3893
|
}
|
|
3784
|
-
} catch (err) {
|
|
3785
|
-
console.error(`Warning: skipping file ${file.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3786
3894
|
}
|
|
3787
3895
|
}
|
|
3788
3896
|
}
|
|
@@ -6614,18 +6722,22 @@ async function searchFilesViaSearchApi(client, input) {
|
|
|
6614
6722
|
if (!id) {
|
|
6615
6723
|
continue;
|
|
6616
6724
|
}
|
|
6617
|
-
const
|
|
6725
|
+
const result = await tryDownloadSlackFile({
|
|
6618
6726
|
auth: input.auth,
|
|
6619
6727
|
url,
|
|
6620
6728
|
destDir: downloadsDir,
|
|
6621
6729
|
preferredName: `${id}${ext ? `.${ext}` : ""}`
|
|
6622
6730
|
});
|
|
6731
|
+
if (!result.ok) {
|
|
6732
|
+
console.warn(`Warning: skipping file ${id}: ${result.error}`);
|
|
6733
|
+
continue;
|
|
6734
|
+
}
|
|
6623
6735
|
const title = (getString(f.title) || getString(f.name) || "").trim();
|
|
6624
6736
|
out.push({
|
|
6625
6737
|
title: title || undefined,
|
|
6626
6738
|
mimetype,
|
|
6627
6739
|
mode,
|
|
6628
|
-
path
|
|
6740
|
+
path: result.path
|
|
6629
6741
|
});
|
|
6630
6742
|
if (out.length >= input.limit) {
|
|
6631
6743
|
break;
|
|
@@ -6680,17 +6792,21 @@ async function searchFilesInChannelsFallback(client, input) {
|
|
|
6680
6792
|
if (!id) {
|
|
6681
6793
|
continue;
|
|
6682
6794
|
}
|
|
6683
|
-
const
|
|
6795
|
+
const result = await tryDownloadSlackFile({
|
|
6684
6796
|
auth: input.auth,
|
|
6685
6797
|
url,
|
|
6686
6798
|
destDir: downloadsDir,
|
|
6687
6799
|
preferredName: `${id}${ext ? `.${ext}` : ""}`
|
|
6688
6800
|
});
|
|
6801
|
+
if (!result.ok) {
|
|
6802
|
+
console.warn(`Warning: skipping file ${id}: ${result.error}`);
|
|
6803
|
+
continue;
|
|
6804
|
+
}
|
|
6689
6805
|
out.push({
|
|
6690
6806
|
title: title || undefined,
|
|
6691
6807
|
mimetype,
|
|
6692
6808
|
mode,
|
|
6693
|
-
path
|
|
6809
|
+
path: result.path
|
|
6694
6810
|
});
|
|
6695
6811
|
if (out.length >= input.limit) {
|
|
6696
6812
|
return out;
|
|
@@ -6906,13 +7022,25 @@ async function downloadFilesForMessage(input) {
|
|
|
6906
7022
|
continue;
|
|
6907
7023
|
}
|
|
6908
7024
|
const ext = inferExt(f);
|
|
6909
|
-
const
|
|
7025
|
+
const result = await tryDownloadSlackFile({
|
|
6910
7026
|
auth: input.auth,
|
|
6911
7027
|
url,
|
|
6912
7028
|
destDir: input.downloadsDir,
|
|
6913
7029
|
preferredName: `${f.id}${ext ? `.${ext}` : ""}`
|
|
6914
7030
|
});
|
|
6915
|
-
|
|
7031
|
+
if (!result.ok) {
|
|
7032
|
+
input.downloadedPaths[f.id] = {
|
|
7033
|
+
...result,
|
|
7034
|
+
path: await writeDownloadErrorFile({
|
|
7035
|
+
destDir: input.downloadsDir,
|
|
7036
|
+
fileId: f.id,
|
|
7037
|
+
error: result.error
|
|
7038
|
+
})
|
|
7039
|
+
};
|
|
7040
|
+
console.warn(`Warning: file ${f.id}: ${result.error}`);
|
|
7041
|
+
} else {
|
|
7042
|
+
input.downloadedPaths[f.id] = result;
|
|
7043
|
+
}
|
|
6916
7044
|
}
|
|
6917
7045
|
}
|
|
6918
7046
|
function messageSummaryFromApiMessage(channelId, msg) {
|
|
@@ -7078,6 +7206,569 @@ function registerSearchCommand(input) {
|
|
|
7078
7206
|
create({ kind: "files", name: "files", desc: "Search files" });
|
|
7079
7207
|
}
|
|
7080
7208
|
|
|
7209
|
+
// src/slack/later.ts
|
|
7210
|
+
async function fetchLaterItems(client, options) {
|
|
7211
|
+
const stateFilter = options?.state ?? "in_progress";
|
|
7212
|
+
const limit = options?.limit ?? 20;
|
|
7213
|
+
const maxBodyChars = options?.maxBodyChars ?? 4000;
|
|
7214
|
+
const countsOnly = options?.countsOnly ?? false;
|
|
7215
|
+
let currentCursor = options?.cursor;
|
|
7216
|
+
let allRawItems = [];
|
|
7217
|
+
let counts = {};
|
|
7218
|
+
let nextCursor;
|
|
7219
|
+
while (true) {
|
|
7220
|
+
const resp = await client.api("saved.list", {
|
|
7221
|
+
limit: 50,
|
|
7222
|
+
cursor: currentCursor
|
|
7223
|
+
});
|
|
7224
|
+
if (!currentCursor) {
|
|
7225
|
+
counts = isRecord5(resp.counts) ? resp.counts : {};
|
|
7226
|
+
}
|
|
7227
|
+
const pageRawItems = asArray2(resp.saved_items).filter(isRecord5);
|
|
7228
|
+
allRawItems.push(...pageRawItems);
|
|
7229
|
+
const meta = isRecord5(resp.response_metadata) ? resp.response_metadata : null;
|
|
7230
|
+
nextCursor = meta ? getString4(meta.next_cursor) : undefined;
|
|
7231
|
+
if (countsOnly) {
|
|
7232
|
+
break;
|
|
7233
|
+
}
|
|
7234
|
+
let filtered2 = allRawItems.filter((item) => getString4(item.item_type) === "message");
|
|
7235
|
+
if (stateFilter !== "all") {
|
|
7236
|
+
filtered2 = filtered2.filter((item) => getString4(item.state) === stateFilter);
|
|
7237
|
+
}
|
|
7238
|
+
if (filtered2.length >= limit || !nextCursor) {
|
|
7239
|
+
break;
|
|
7240
|
+
}
|
|
7241
|
+
currentCursor = nextCursor;
|
|
7242
|
+
}
|
|
7243
|
+
const result = {
|
|
7244
|
+
counts: {
|
|
7245
|
+
in_progress: getNumber3(counts.uncompleted_count) ?? 0,
|
|
7246
|
+
archived: getNumber3(counts.archived_count) ?? 0,
|
|
7247
|
+
completed: getNumber3(counts.completed_count) ?? 0,
|
|
7248
|
+
total: getNumber3(counts.total_count) ?? 0
|
|
7249
|
+
},
|
|
7250
|
+
items: [],
|
|
7251
|
+
next_cursor: nextCursor
|
|
7252
|
+
};
|
|
7253
|
+
if (countsOnly) {
|
|
7254
|
+
return result;
|
|
7255
|
+
}
|
|
7256
|
+
let filtered = allRawItems.filter((item) => getString4(item.item_type) === "message");
|
|
7257
|
+
if (stateFilter !== "all") {
|
|
7258
|
+
filtered = filtered.filter((item) => getString4(item.state) === stateFilter);
|
|
7259
|
+
}
|
|
7260
|
+
filtered = filtered.slice(0, limit);
|
|
7261
|
+
result.items = await Promise.all(filtered.map(async (item) => {
|
|
7262
|
+
const channelId = getString4(item.item_id) ?? "";
|
|
7263
|
+
const ts = getString4(item.ts) ?? "";
|
|
7264
|
+
const state = getString4(item.state) ?? "in_progress";
|
|
7265
|
+
const dateSaved = getNumber3(item.date_created) ?? 0;
|
|
7266
|
+
const dateCompleted = getNumber3(item.date_completed);
|
|
7267
|
+
let channelName;
|
|
7268
|
+
let message;
|
|
7269
|
+
try {
|
|
7270
|
+
const info = await client.api("conversations.info", {
|
|
7271
|
+
channel: channelId
|
|
7272
|
+
});
|
|
7273
|
+
const ch = isRecord5(info.channel) ? info.channel : null;
|
|
7274
|
+
if (ch) {
|
|
7275
|
+
channelName = getString4(ch.name) ?? getString4(ch.name_normalized) ?? undefined;
|
|
7276
|
+
if (ch.is_im && !channelName) {
|
|
7277
|
+
const userId = getString4(ch.user);
|
|
7278
|
+
if (userId) {
|
|
7279
|
+
try {
|
|
7280
|
+
const userInfo = await client.api("users.info", { user: userId });
|
|
7281
|
+
const u = isRecord5(userInfo.user) ? userInfo.user : null;
|
|
7282
|
+
const profile = u && isRecord5(u.profile) ? u.profile : null;
|
|
7283
|
+
channelName = getString4(profile?.display_name) || getString4(u?.real_name) || getString4(u?.name) || undefined;
|
|
7284
|
+
} catch {}
|
|
7285
|
+
}
|
|
7286
|
+
}
|
|
7287
|
+
}
|
|
7288
|
+
} catch {}
|
|
7289
|
+
if (ts) {
|
|
7290
|
+
try {
|
|
7291
|
+
const history = await client.api("conversations.history", {
|
|
7292
|
+
channel: channelId,
|
|
7293
|
+
latest: ts,
|
|
7294
|
+
inclusive: true,
|
|
7295
|
+
limit: 1
|
|
7296
|
+
});
|
|
7297
|
+
const msgs = asArray2(history.messages).filter(isRecord5);
|
|
7298
|
+
const msg = msgs.find((m) => getString4(m.ts) === ts);
|
|
7299
|
+
if (msg) {
|
|
7300
|
+
const rendered = renderSlackMessageContent(msg);
|
|
7301
|
+
const content = maxBodyChars >= 0 && rendered.length > maxBodyChars ? `${rendered.slice(0, maxBodyChars)}
|
|
7302
|
+
…` : rendered;
|
|
7303
|
+
message = {
|
|
7304
|
+
author: getString4(msg.user) || getString4(msg.bot_id) ? {
|
|
7305
|
+
user_id: getString4(msg.user) ?? undefined,
|
|
7306
|
+
bot_id: getString4(msg.bot_id) ?? undefined
|
|
7307
|
+
} : undefined,
|
|
7308
|
+
content: content || undefined,
|
|
7309
|
+
thread_ts: getString4(msg.thread_ts) ?? undefined,
|
|
7310
|
+
reply_count: getNumber3(msg.reply_count) ?? undefined
|
|
7311
|
+
};
|
|
7312
|
+
}
|
|
7313
|
+
} catch {}
|
|
7314
|
+
}
|
|
7315
|
+
return {
|
|
7316
|
+
channel_id: channelId,
|
|
7317
|
+
channel_name: channelName,
|
|
7318
|
+
ts,
|
|
7319
|
+
state,
|
|
7320
|
+
date_saved: dateSaved,
|
|
7321
|
+
date_completed: dateCompleted && dateCompleted > 0 ? dateCompleted : undefined,
|
|
7322
|
+
message
|
|
7323
|
+
};
|
|
7324
|
+
}));
|
|
7325
|
+
return result;
|
|
7326
|
+
}
|
|
7327
|
+
async function updateLaterMark(client, input) {
|
|
7328
|
+
await client.apiMultipart("saved.update", {
|
|
7329
|
+
item_id: input.channelId,
|
|
7330
|
+
item_type: "message",
|
|
7331
|
+
ts: input.ts,
|
|
7332
|
+
mark: input.mark
|
|
7333
|
+
});
|
|
7334
|
+
}
|
|
7335
|
+
async function saveLater(client, input) {
|
|
7336
|
+
await client.api("saved.add", {
|
|
7337
|
+
item_id: input.channelId,
|
|
7338
|
+
item_type: "message",
|
|
7339
|
+
ts: input.ts
|
|
7340
|
+
});
|
|
7341
|
+
}
|
|
7342
|
+
async function removeLater(client, input) {
|
|
7343
|
+
await client.api("saved.delete", {
|
|
7344
|
+
item_id: input.channelId,
|
|
7345
|
+
item_type: "message",
|
|
7346
|
+
ts: input.ts
|
|
7347
|
+
});
|
|
7348
|
+
}
|
|
7349
|
+
async function setLaterReminder(client, input) {
|
|
7350
|
+
await client.apiMultipart("saved.update", {
|
|
7351
|
+
item_id: input.channelId,
|
|
7352
|
+
item_type: "message",
|
|
7353
|
+
ts: input.ts,
|
|
7354
|
+
date_due: String(input.dateDue)
|
|
7355
|
+
});
|
|
7356
|
+
}
|
|
7357
|
+
function parseReminderDuration(input) {
|
|
7358
|
+
const now = Math.floor(Date.now() / 1000);
|
|
7359
|
+
const trimmed = input.trim().toLowerCase();
|
|
7360
|
+
const relMatch = trimmed.match(/^(\d+(?:\.\d+)?)\s*(m|min|mins|minutes?|h|hr|hrs|hours?|d|days?)$/);
|
|
7361
|
+
if (relMatch) {
|
|
7362
|
+
const amount = Number.parseFloat(relMatch[1]);
|
|
7363
|
+
const unit = relMatch[2].charAt(0);
|
|
7364
|
+
if (unit === "m") {
|
|
7365
|
+
return now + amount * 60;
|
|
7366
|
+
}
|
|
7367
|
+
if (unit === "h") {
|
|
7368
|
+
return now + amount * 3600;
|
|
7369
|
+
}
|
|
7370
|
+
if (unit === "d") {
|
|
7371
|
+
return now + amount * 86400;
|
|
7372
|
+
}
|
|
7373
|
+
}
|
|
7374
|
+
const tomorrow9am = getNext9am(1);
|
|
7375
|
+
if (trimmed === "tomorrow") {
|
|
7376
|
+
return tomorrow9am;
|
|
7377
|
+
}
|
|
7378
|
+
const dayNames = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
7379
|
+
const dayIndex = dayNames.indexOf(trimmed);
|
|
7380
|
+
if (dayIndex >= 0) {
|
|
7381
|
+
const today = new Date;
|
|
7382
|
+
const currentDay = today.getDay();
|
|
7383
|
+
let daysUntil = dayIndex - currentDay;
|
|
7384
|
+
if (daysUntil <= 0) {
|
|
7385
|
+
daysUntil += 7;
|
|
7386
|
+
}
|
|
7387
|
+
return getNext9am(daysUntil);
|
|
7388
|
+
}
|
|
7389
|
+
const asNum = Number(trimmed);
|
|
7390
|
+
if (!Number.isNaN(asNum) && asNum > 1e9) {
|
|
7391
|
+
return asNum;
|
|
7392
|
+
}
|
|
7393
|
+
throw new Error(`Invalid duration: "${input}". Use: 30m, 1h, 3h, 2d, tomorrow, monday, or a unix timestamp.`);
|
|
7394
|
+
}
|
|
7395
|
+
function getNext9am(daysFromNow) {
|
|
7396
|
+
const date = new Date;
|
|
7397
|
+
date.setDate(date.getDate() + daysFromNow);
|
|
7398
|
+
date.setHours(9, 0, 0, 0);
|
|
7399
|
+
return Math.floor(date.getTime() / 1000);
|
|
7400
|
+
}
|
|
7401
|
+
function isRecord5(value) {
|
|
7402
|
+
return typeof value === "object" && value !== null;
|
|
7403
|
+
}
|
|
7404
|
+
function asArray2(value) {
|
|
7405
|
+
return Array.isArray(value) ? value : [];
|
|
7406
|
+
}
|
|
7407
|
+
function getString4(value) {
|
|
7408
|
+
return typeof value === "string" ? value : undefined;
|
|
7409
|
+
}
|
|
7410
|
+
function getNumber3(value) {
|
|
7411
|
+
return typeof value === "number" ? value : undefined;
|
|
7412
|
+
}
|
|
7413
|
+
|
|
7414
|
+
// src/cli/later-command.ts
|
|
7415
|
+
function resolveTargetRef(input) {
|
|
7416
|
+
const { targetInput, options, ctx } = input;
|
|
7417
|
+
const target = parseMsgTarget(targetInput);
|
|
7418
|
+
if (target.kind === "url") {
|
|
7419
|
+
return {
|
|
7420
|
+
workspaceUrl: target.ref.workspace_url,
|
|
7421
|
+
getChannelAndTs: async () => ({
|
|
7422
|
+
channelId: target.ref.channel_id,
|
|
7423
|
+
ts: target.ref.message_ts
|
|
7424
|
+
})
|
|
7425
|
+
};
|
|
7426
|
+
}
|
|
7427
|
+
const workspaceUrl = ctx.effectiveWorkspaceUrl(options.workspace);
|
|
7428
|
+
const ts = options.ts?.trim();
|
|
7429
|
+
if (!ts) {
|
|
7430
|
+
throw new Error('When targeting a channel, you must pass --ts "<seconds>.<micros>"');
|
|
7431
|
+
}
|
|
7432
|
+
return {
|
|
7433
|
+
workspaceUrl,
|
|
7434
|
+
getChannelAndTs: async (client) => {
|
|
7435
|
+
const channelId = await resolveChannelId(client, target.kind === "channel" ? target.channel : targetInput);
|
|
7436
|
+
return { channelId, ts };
|
|
7437
|
+
}
|
|
7438
|
+
};
|
|
7439
|
+
}
|
|
7440
|
+
function registerLaterCommand(input) {
|
|
7441
|
+
const laterCmd = input.program.command("later").description("Manage saved-for-later messages (Slack's Later tab)");
|
|
7442
|
+
laterCmd.command("list", { isDefault: true }).description("List saved-for-later messages").option("--workspace <url>", "Workspace URL (defaults to your configured workspace)").option("--state <state>", "Filter by state: in_progress (default), archived, completed, all", "in_progress").option("--limit <n>", "Max items to show (default 20)", "20").option("--max-body-chars <n>", "Max content characters per message (default 4000, -1 for unlimited)", "4000").option("--counts-only", "Only show counts per state, skip message content").action(async (options) => {
|
|
7443
|
+
try {
|
|
7444
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
|
|
7445
|
+
const state = parseState(options.state ?? "in_progress");
|
|
7446
|
+
const payload = await input.ctx.withAutoRefresh({
|
|
7447
|
+
workspaceUrl,
|
|
7448
|
+
work: async () => {
|
|
7449
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
7450
|
+
return fetchLaterItems(client, {
|
|
7451
|
+
state,
|
|
7452
|
+
limit: Number.parseInt(options.limit ?? "20", 10),
|
|
7453
|
+
maxBodyChars: Number.parseInt(options.maxBodyChars ?? "4000", 10),
|
|
7454
|
+
countsOnly: options.countsOnly
|
|
7455
|
+
});
|
|
7456
|
+
}
|
|
7457
|
+
});
|
|
7458
|
+
console.log(JSON.stringify(pruneEmpty(payload), null, 2));
|
|
7459
|
+
} catch (err) {
|
|
7460
|
+
console.error(input.ctx.errorMessage(err));
|
|
7461
|
+
process.exitCode = 1;
|
|
7462
|
+
}
|
|
7463
|
+
});
|
|
7464
|
+
laterCmd.command("complete").description("Mark a saved message as completed").argument("<target>", "Slack message URL or channel ID").option("--workspace <url>", "Workspace URL").option("--ts <ts>", "Message ts (required when using channel ID)").action(async (...args) => {
|
|
7465
|
+
const [targetInput, options] = args;
|
|
7466
|
+
try {
|
|
7467
|
+
const ref = resolveTargetRef({ targetInput, options, ctx: input.ctx });
|
|
7468
|
+
await input.ctx.withAutoRefresh({
|
|
7469
|
+
workspaceUrl: ref.workspaceUrl,
|
|
7470
|
+
work: async () => {
|
|
7471
|
+
const { client } = await input.ctx.getClientForWorkspace(ref.workspaceUrl);
|
|
7472
|
+
const { channelId, ts } = await ref.getChannelAndTs(client);
|
|
7473
|
+
await updateLaterMark(client, { channelId, ts, mark: "completed" });
|
|
7474
|
+
}
|
|
7475
|
+
});
|
|
7476
|
+
console.log(JSON.stringify({ ok: true }));
|
|
7477
|
+
} catch (err) {
|
|
7478
|
+
console.error(input.ctx.errorMessage(err));
|
|
7479
|
+
process.exitCode = 1;
|
|
7480
|
+
}
|
|
7481
|
+
});
|
|
7482
|
+
laterCmd.command("archive").description("Archive a saved message").argument("<target>", "Slack message URL or channel ID").option("--workspace <url>", "Workspace URL").option("--ts <ts>", "Message ts (required when using channel ID)").action(async (...args) => {
|
|
7483
|
+
const [targetInput, options] = args;
|
|
7484
|
+
try {
|
|
7485
|
+
const ref = resolveTargetRef({ targetInput, options, ctx: input.ctx });
|
|
7486
|
+
await input.ctx.withAutoRefresh({
|
|
7487
|
+
workspaceUrl: ref.workspaceUrl,
|
|
7488
|
+
work: async () => {
|
|
7489
|
+
const { client } = await input.ctx.getClientForWorkspace(ref.workspaceUrl);
|
|
7490
|
+
const { channelId, ts } = await ref.getChannelAndTs(client);
|
|
7491
|
+
await updateLaterMark(client, { channelId, ts, mark: "archived" });
|
|
7492
|
+
}
|
|
7493
|
+
});
|
|
7494
|
+
console.log(JSON.stringify({ ok: true }));
|
|
7495
|
+
} catch (err) {
|
|
7496
|
+
console.error(input.ctx.errorMessage(err));
|
|
7497
|
+
process.exitCode = 1;
|
|
7498
|
+
}
|
|
7499
|
+
});
|
|
7500
|
+
laterCmd.command("reopen").description("Move a saved message back to in-progress").argument("<target>", "Slack message URL or channel ID").option("--workspace <url>", "Workspace URL").option("--ts <ts>", "Message ts (required when using channel ID)").action(async (...args) => {
|
|
7501
|
+
const [targetInput, options] = args;
|
|
7502
|
+
try {
|
|
7503
|
+
const ref = resolveTargetRef({ targetInput, options, ctx: input.ctx });
|
|
7504
|
+
await input.ctx.withAutoRefresh({
|
|
7505
|
+
workspaceUrl: ref.workspaceUrl,
|
|
7506
|
+
work: async () => {
|
|
7507
|
+
const { client } = await input.ctx.getClientForWorkspace(ref.workspaceUrl);
|
|
7508
|
+
const { channelId, ts } = await ref.getChannelAndTs(client);
|
|
7509
|
+
await Promise.allSettled([
|
|
7510
|
+
updateLaterMark(client, { channelId, ts, mark: "uncompleted" }),
|
|
7511
|
+
updateLaterMark(client, { channelId, ts, mark: "unarchived" })
|
|
7512
|
+
]);
|
|
7513
|
+
}
|
|
7514
|
+
});
|
|
7515
|
+
console.log(JSON.stringify({ ok: true }));
|
|
7516
|
+
} catch (err) {
|
|
7517
|
+
console.error(input.ctx.errorMessage(err));
|
|
7518
|
+
process.exitCode = 1;
|
|
7519
|
+
}
|
|
7520
|
+
});
|
|
7521
|
+
laterCmd.command("save").description("Save a message for later").argument("<target>", "Slack message URL or channel ID").option("--workspace <url>", "Workspace URL").option("--ts <ts>", "Message ts (required when using channel ID)").action(async (...args) => {
|
|
7522
|
+
const [targetInput, options] = args;
|
|
7523
|
+
try {
|
|
7524
|
+
const ref = resolveTargetRef({ targetInput, options, ctx: input.ctx });
|
|
7525
|
+
await input.ctx.withAutoRefresh({
|
|
7526
|
+
workspaceUrl: ref.workspaceUrl,
|
|
7527
|
+
work: async () => {
|
|
7528
|
+
const { client } = await input.ctx.getClientForWorkspace(ref.workspaceUrl);
|
|
7529
|
+
const { channelId, ts } = await ref.getChannelAndTs(client);
|
|
7530
|
+
await saveLater(client, { channelId, ts });
|
|
7531
|
+
}
|
|
7532
|
+
});
|
|
7533
|
+
console.log(JSON.stringify({ ok: true }));
|
|
7534
|
+
} catch (err) {
|
|
7535
|
+
console.error(input.ctx.errorMessage(err));
|
|
7536
|
+
process.exitCode = 1;
|
|
7537
|
+
}
|
|
7538
|
+
});
|
|
7539
|
+
laterCmd.command("remove").description("Remove a message from Later entirely").argument("<target>", "Slack message URL or channel ID").option("--workspace <url>", "Workspace URL").option("--ts <ts>", "Message ts (required when using channel ID)").action(async (...args) => {
|
|
7540
|
+
const [targetInput, options] = args;
|
|
7541
|
+
try {
|
|
7542
|
+
const ref = resolveTargetRef({ targetInput, options, ctx: input.ctx });
|
|
7543
|
+
await input.ctx.withAutoRefresh({
|
|
7544
|
+
workspaceUrl: ref.workspaceUrl,
|
|
7545
|
+
work: async () => {
|
|
7546
|
+
const { client } = await input.ctx.getClientForWorkspace(ref.workspaceUrl);
|
|
7547
|
+
const { channelId, ts } = await ref.getChannelAndTs(client);
|
|
7548
|
+
await removeLater(client, { channelId, ts });
|
|
7549
|
+
}
|
|
7550
|
+
});
|
|
7551
|
+
console.log(JSON.stringify({ ok: true }));
|
|
7552
|
+
} catch (err) {
|
|
7553
|
+
console.error(input.ctx.errorMessage(err));
|
|
7554
|
+
process.exitCode = 1;
|
|
7555
|
+
}
|
|
7556
|
+
});
|
|
7557
|
+
laterCmd.command("remind").description("Set a reminder for a saved message").argument("<target>", "Slack message URL or channel ID").requiredOption("--in <duration>", "When to remind: 30m, 1h, 3h, 2d, tomorrow, monday, etc.").option("--workspace <url>", "Workspace URL").option("--ts <ts>", "Message ts (required when using channel ID)").action(async (...args) => {
|
|
7558
|
+
const [targetInput, options] = args;
|
|
7559
|
+
try {
|
|
7560
|
+
const ref = resolveTargetRef({ targetInput, options, ctx: input.ctx });
|
|
7561
|
+
const reminderTime = parseReminderDuration(options.in);
|
|
7562
|
+
await input.ctx.withAutoRefresh({
|
|
7563
|
+
workspaceUrl: ref.workspaceUrl,
|
|
7564
|
+
work: async () => {
|
|
7565
|
+
const { client } = await input.ctx.getClientForWorkspace(ref.workspaceUrl);
|
|
7566
|
+
const { channelId, ts } = await ref.getChannelAndTs(client);
|
|
7567
|
+
await setLaterReminder(client, { channelId, ts, dateDue: reminderTime });
|
|
7568
|
+
}
|
|
7569
|
+
});
|
|
7570
|
+
console.log(JSON.stringify({
|
|
7571
|
+
ok: true,
|
|
7572
|
+
remind_at: reminderTime
|
|
7573
|
+
}));
|
|
7574
|
+
} catch (err) {
|
|
7575
|
+
console.error(input.ctx.errorMessage(err));
|
|
7576
|
+
process.exitCode = 1;
|
|
7577
|
+
}
|
|
7578
|
+
});
|
|
7579
|
+
}
|
|
7580
|
+
function parseState(value) {
|
|
7581
|
+
const v = value.toLowerCase().trim();
|
|
7582
|
+
if (v === "in_progress" || v === "in-progress" || v === "active" || v === "open") {
|
|
7583
|
+
return "in_progress";
|
|
7584
|
+
}
|
|
7585
|
+
if (v === "archived" || v === "archive") {
|
|
7586
|
+
return "archived";
|
|
7587
|
+
}
|
|
7588
|
+
if (v === "completed" || v === "complete" || v === "done") {
|
|
7589
|
+
return "completed";
|
|
7590
|
+
}
|
|
7591
|
+
if (v === "all") {
|
|
7592
|
+
return "all";
|
|
7593
|
+
}
|
|
7594
|
+
return "in_progress";
|
|
7595
|
+
}
|
|
7596
|
+
|
|
7597
|
+
// src/slack/unreads.ts
|
|
7598
|
+
async function fetchUnreads(client, options) {
|
|
7599
|
+
const includeMessages = options?.includeMessages ?? true;
|
|
7600
|
+
const maxMessages = options?.maxMessagesPerChannel ?? 10;
|
|
7601
|
+
const maxBodyChars = options?.maxBodyChars ?? 4000;
|
|
7602
|
+
const skipSystem = options?.skipSystemMessages ?? true;
|
|
7603
|
+
const resp = await client.api("client.counts", {
|
|
7604
|
+
thread_count_by_channel: true
|
|
7605
|
+
});
|
|
7606
|
+
const channels = asArray3(resp.channels).filter(isRecord6);
|
|
7607
|
+
const mpims = asArray3(resp.mpims).filter(isRecord6);
|
|
7608
|
+
const ims = asArray3(resp.ims).filter(isRecord6);
|
|
7609
|
+
const allEntries = [
|
|
7610
|
+
...channels.map((c) => ({ ...c, type: "channel" })),
|
|
7611
|
+
...mpims.map((c) => ({ ...c, type: "mpim" })),
|
|
7612
|
+
...ims.map((c) => ({ ...c, type: "dm" }))
|
|
7613
|
+
];
|
|
7614
|
+
const withUnreads = allEntries.filter((c) => c.has_unreads);
|
|
7615
|
+
const channelInfos = await Promise.all(withUnreads.map(async (entry) => {
|
|
7616
|
+
const channelInfoPromise = (async () => {
|
|
7617
|
+
let name;
|
|
7618
|
+
let { type } = entry;
|
|
7619
|
+
try {
|
|
7620
|
+
const info = await client.api("conversations.info", {
|
|
7621
|
+
channel: entry.id
|
|
7622
|
+
});
|
|
7623
|
+
const ch = isRecord6(info.channel) ? info.channel : null;
|
|
7624
|
+
if (ch) {
|
|
7625
|
+
name = getString5(ch.name) ?? getString5(ch.name_normalized) ?? undefined;
|
|
7626
|
+
if (ch.is_im) {
|
|
7627
|
+
type = "dm";
|
|
7628
|
+
const userId = getString5(ch.user);
|
|
7629
|
+
if (userId && !name) {
|
|
7630
|
+
try {
|
|
7631
|
+
const userInfo = await client.api("users.info", { user: userId });
|
|
7632
|
+
const u = isRecord6(userInfo.user) ? userInfo.user : null;
|
|
7633
|
+
const profile = u && isRecord6(u.profile) ? u.profile : null;
|
|
7634
|
+
name = getString5(profile?.display_name) || getString5(u?.real_name) || getString5(u?.name) || undefined;
|
|
7635
|
+
} catch {}
|
|
7636
|
+
}
|
|
7637
|
+
} else if (ch.is_mpim) {
|
|
7638
|
+
type = "mpim";
|
|
7639
|
+
} else if (ch.is_group || ch.is_private) {
|
|
7640
|
+
type = "group";
|
|
7641
|
+
} else {
|
|
7642
|
+
type = "channel";
|
|
7643
|
+
}
|
|
7644
|
+
}
|
|
7645
|
+
} catch {}
|
|
7646
|
+
return { name, type };
|
|
7647
|
+
})();
|
|
7648
|
+
const historyPromise = (async () => {
|
|
7649
|
+
let messages;
|
|
7650
|
+
let unreadCount = entry.unread_count_display ?? entry.unread_count ?? (entry.has_unreads ? 1 : 0);
|
|
7651
|
+
if (includeMessages && entry.last_read) {
|
|
7652
|
+
try {
|
|
7653
|
+
const history = await client.api("conversations.history", {
|
|
7654
|
+
channel: entry.id,
|
|
7655
|
+
oldest: entry.last_read,
|
|
7656
|
+
limit: maxMessages,
|
|
7657
|
+
inclusive: false
|
|
7658
|
+
});
|
|
7659
|
+
let msgs = asArray3(history.messages).filter(isRecord6);
|
|
7660
|
+
if (skipSystem) {
|
|
7661
|
+
msgs = msgs.filter((m) => {
|
|
7662
|
+
const subtype = getString5(m.subtype);
|
|
7663
|
+
if (!subtype) {
|
|
7664
|
+
return true;
|
|
7665
|
+
}
|
|
7666
|
+
const systemSubtypes = [
|
|
7667
|
+
"channel_join",
|
|
7668
|
+
"channel_leave",
|
|
7669
|
+
"channel_topic",
|
|
7670
|
+
"channel_purpose",
|
|
7671
|
+
"channel_name",
|
|
7672
|
+
"channel_archive",
|
|
7673
|
+
"channel_unarchive",
|
|
7674
|
+
"group_join",
|
|
7675
|
+
"group_leave",
|
|
7676
|
+
"group_topic",
|
|
7677
|
+
"group_purpose",
|
|
7678
|
+
"group_name",
|
|
7679
|
+
"group_archive",
|
|
7680
|
+
"group_unarchive"
|
|
7681
|
+
];
|
|
7682
|
+
return !systemSubtypes.includes(subtype);
|
|
7683
|
+
});
|
|
7684
|
+
}
|
|
7685
|
+
if (entry.unread_count_display === undefined && entry.unread_count === undefined) {
|
|
7686
|
+
unreadCount = msgs.length;
|
|
7687
|
+
if (history.has_more) {
|
|
7688
|
+
unreadCount = Math.max(unreadCount, 2);
|
|
7689
|
+
}
|
|
7690
|
+
}
|
|
7691
|
+
messages = msgs.map((m) => {
|
|
7692
|
+
const rendered = renderSlackMessageContent(m);
|
|
7693
|
+
const content = maxBodyChars >= 0 && rendered.length > maxBodyChars ? `${rendered.slice(0, maxBodyChars)}
|
|
7694
|
+
...` : rendered;
|
|
7695
|
+
return {
|
|
7696
|
+
ts: getString5(m.ts) ?? "",
|
|
7697
|
+
author: getString5(m.user) || getString5(m.bot_id) ? {
|
|
7698
|
+
user_id: getString5(m.user) ?? undefined,
|
|
7699
|
+
bot_id: getString5(m.bot_id) ?? undefined
|
|
7700
|
+
} : undefined,
|
|
7701
|
+
content: content || undefined,
|
|
7702
|
+
thread_ts: getString5(m.thread_ts) ?? undefined,
|
|
7703
|
+
reply_count: getNumber4(m.reply_count) ?? undefined
|
|
7704
|
+
};
|
|
7705
|
+
});
|
|
7706
|
+
messages.sort((a, b) => Number.parseFloat(a.ts) - Number.parseFloat(b.ts));
|
|
7707
|
+
} catch {}
|
|
7708
|
+
}
|
|
7709
|
+
return { messages, unreadCount };
|
|
7710
|
+
})();
|
|
7711
|
+
const [channelData, historyData] = await Promise.all([channelInfoPromise, historyPromise]);
|
|
7712
|
+
return {
|
|
7713
|
+
channel_id: entry.id,
|
|
7714
|
+
channel_name: channelData.name,
|
|
7715
|
+
channel_type: channelData.type,
|
|
7716
|
+
unread_count: historyData.unreadCount,
|
|
7717
|
+
mention_count: entry.mention_count ?? 0,
|
|
7718
|
+
messages: historyData.messages
|
|
7719
|
+
};
|
|
7720
|
+
}));
|
|
7721
|
+
channelInfos.sort((a, b) => {
|
|
7722
|
+
if (a.mention_count !== b.mention_count) {
|
|
7723
|
+
return b.mention_count - a.mention_count;
|
|
7724
|
+
}
|
|
7725
|
+
return b.unread_count - a.unread_count;
|
|
7726
|
+
});
|
|
7727
|
+
const threads = isRecord6(resp.threads) ? resp.threads : null;
|
|
7728
|
+
const threadInfo = threads?.has_unreads ? {
|
|
7729
|
+
has_unreads: true,
|
|
7730
|
+
mention_count: threads.mention_count ?? 0
|
|
7731
|
+
} : null;
|
|
7732
|
+
return { channels: channelInfos, threads: threadInfo };
|
|
7733
|
+
}
|
|
7734
|
+
function isRecord6(value) {
|
|
7735
|
+
return typeof value === "object" && value !== null;
|
|
7736
|
+
}
|
|
7737
|
+
function asArray3(value) {
|
|
7738
|
+
return Array.isArray(value) ? value : [];
|
|
7739
|
+
}
|
|
7740
|
+
function getString5(value) {
|
|
7741
|
+
return typeof value === "string" ? value : undefined;
|
|
7742
|
+
}
|
|
7743
|
+
function getNumber4(value) {
|
|
7744
|
+
return typeof value === "number" ? value : undefined;
|
|
7745
|
+
}
|
|
7746
|
+
|
|
7747
|
+
// src/cli/unreads-command.ts
|
|
7748
|
+
function registerUnreadsCommand(input) {
|
|
7749
|
+
input.program.command("unreads").description("Show all unread messages across channels, DMs, and threads").option("--workspace <url>", "Workspace URL (defaults to your configured workspace)").option("--counts-only", "Only show unread counts, do not fetch message content").option("--max-messages <n>", "Max unread messages to fetch per channel (default 10)", "10").option("--max-body-chars <n>", "Max content characters per message (default 4000, -1 for unlimited)", "4000").option("--include-system", "Include system messages (joins, leaves, topic changes, etc.)").action(async (options) => {
|
|
7750
|
+
try {
|
|
7751
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
|
|
7752
|
+
const payload = await input.ctx.withAutoRefresh({
|
|
7753
|
+
workspaceUrl,
|
|
7754
|
+
work: async () => {
|
|
7755
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
7756
|
+
return fetchUnreads(client, {
|
|
7757
|
+
includeMessages: !options.countsOnly,
|
|
7758
|
+
maxMessagesPerChannel: Number.parseInt(options.maxMessages ?? "10", 10),
|
|
7759
|
+
maxBodyChars: Number.parseInt(options.maxBodyChars ?? "4000", 10),
|
|
7760
|
+
skipSystemMessages: !options.includeSystem
|
|
7761
|
+
});
|
|
7762
|
+
}
|
|
7763
|
+
});
|
|
7764
|
+
console.log(JSON.stringify(pruneEmpty(payload), null, 2));
|
|
7765
|
+
} catch (err) {
|
|
7766
|
+
console.error(input.ctx.errorMessage(err));
|
|
7767
|
+
process.exitCode = 1;
|
|
7768
|
+
}
|
|
7769
|
+
});
|
|
7770
|
+
}
|
|
7771
|
+
|
|
7081
7772
|
// src/lib/update.ts
|
|
7082
7773
|
import { execSync as execSync2 } from "node:child_process";
|
|
7083
7774
|
import { createHash } from "node:crypto";
|
|
@@ -7828,6 +8519,274 @@ function registerChannelCommand(input) {
|
|
|
7828
8519
|
});
|
|
7829
8520
|
}
|
|
7830
8521
|
|
|
8522
|
+
// src/slack/workflows.ts
|
|
8523
|
+
async function listChannelWorkflows(client, channelId) {
|
|
8524
|
+
const [bookmarked, featured] = await Promise.all([
|
|
8525
|
+
listBookmarkedWorkflows(client, channelId),
|
|
8526
|
+
listFeaturedWorkflows(client, channelId)
|
|
8527
|
+
]);
|
|
8528
|
+
const featuredIds = new Set(featured.map((f) => f.trigger_id));
|
|
8529
|
+
const seen = new Set;
|
|
8530
|
+
const workflows = [];
|
|
8531
|
+
for (const bk of bookmarked) {
|
|
8532
|
+
if (bk.trigger_id) {
|
|
8533
|
+
seen.add(bk.trigger_id);
|
|
8534
|
+
}
|
|
8535
|
+
workflows.push({
|
|
8536
|
+
title: bk.title,
|
|
8537
|
+
trigger_id: bk.trigger_id ?? "",
|
|
8538
|
+
link: bk.link,
|
|
8539
|
+
app_id: bk.app_id,
|
|
8540
|
+
featured: bk.trigger_id ? featuredIds.has(bk.trigger_id) : false
|
|
8541
|
+
});
|
|
8542
|
+
}
|
|
8543
|
+
for (const ft of featured) {
|
|
8544
|
+
if (!seen.has(ft.trigger_id)) {
|
|
8545
|
+
workflows.push({
|
|
8546
|
+
title: ft.title,
|
|
8547
|
+
trigger_id: ft.trigger_id,
|
|
8548
|
+
featured: true
|
|
8549
|
+
});
|
|
8550
|
+
}
|
|
8551
|
+
}
|
|
8552
|
+
return { channel_id: channelId, workflows };
|
|
8553
|
+
}
|
|
8554
|
+
async function listBookmarkedWorkflows(client, channelId) {
|
|
8555
|
+
const resp = await client.api("bookmarks.list", {
|
|
8556
|
+
channel_id: channelId
|
|
8557
|
+
});
|
|
8558
|
+
return asArray(resp.bookmarks).filter(isRecord).filter((b) => {
|
|
8559
|
+
const link = getString(b.link) ?? "";
|
|
8560
|
+
const shortcutId = getString(b.shortcut_id);
|
|
8561
|
+
return shortcutId || link.includes("slack.com/shortcuts/");
|
|
8562
|
+
}).map((b) => {
|
|
8563
|
+
const link = getString(b.link);
|
|
8564
|
+
const shortcutId = getString(b.shortcut_id);
|
|
8565
|
+
const triggerId = shortcutId || extractTriggerId(link);
|
|
8566
|
+
return {
|
|
8567
|
+
title: getString(b.title) ?? "",
|
|
8568
|
+
trigger_id: triggerId,
|
|
8569
|
+
link,
|
|
8570
|
+
app_id: getString(b.app_id)
|
|
8571
|
+
};
|
|
8572
|
+
});
|
|
8573
|
+
}
|
|
8574
|
+
async function listFeaturedWorkflows(client, channelId) {
|
|
8575
|
+
try {
|
|
8576
|
+
const resp = await client.api("workflows.featured.list", {
|
|
8577
|
+
channel_ids: JSON.stringify([channelId])
|
|
8578
|
+
});
|
|
8579
|
+
const entries = asArray(resp.featured_workflows).filter(isRecord);
|
|
8580
|
+
const entry = entries.find((e) => getString(e.channel_id) === channelId);
|
|
8581
|
+
if (!entry) {
|
|
8582
|
+
return [];
|
|
8583
|
+
}
|
|
8584
|
+
return asArray(entry.triggers).filter(isRecord).map((t) => ({
|
|
8585
|
+
trigger_id: getString(t.id) ?? "",
|
|
8586
|
+
title: getString(t.title) ?? ""
|
|
8587
|
+
})).filter((t) => t.trigger_id);
|
|
8588
|
+
} catch {
|
|
8589
|
+
return [];
|
|
8590
|
+
}
|
|
8591
|
+
}
|
|
8592
|
+
async function previewWorkflow(client, triggerId) {
|
|
8593
|
+
const resp = await client.api("workflows.triggers.preview", {
|
|
8594
|
+
trigger_ids: triggerId
|
|
8595
|
+
});
|
|
8596
|
+
const triggers = asArray(resp.triggers).filter(isRecord);
|
|
8597
|
+
if (triggers.length === 0) {
|
|
8598
|
+
const rejected = asArray(resp.rejected_triggers);
|
|
8599
|
+
if (rejected.length > 0) {
|
|
8600
|
+
throw new Error(`Trigger ${triggerId} was rejected — you may not have access`);
|
|
8601
|
+
}
|
|
8602
|
+
throw new Error(`No preview data returned for trigger ${triggerId}`);
|
|
8603
|
+
}
|
|
8604
|
+
const t = triggers[0];
|
|
8605
|
+
const wf = isRecord(t.workflow) ? t.workflow : {};
|
|
8606
|
+
const wfApp = isRecord(wf.app) ? wf.app : {};
|
|
8607
|
+
const details = isRecord(t.workflow_details) ? t.workflow_details : {};
|
|
8608
|
+
return {
|
|
8609
|
+
trigger_id: getString(t.id) ?? triggerId,
|
|
8610
|
+
type: getString(t.type) ?? "",
|
|
8611
|
+
name: getString(t.name) ?? "",
|
|
8612
|
+
description: getString(t.description) ?? "",
|
|
8613
|
+
shortcut_url: getString(t.shortcut_url),
|
|
8614
|
+
workflow: {
|
|
8615
|
+
id: getString(wf.workflow_id) ?? "",
|
|
8616
|
+
title: getString(wf.title) ?? "",
|
|
8617
|
+
description: getString(wf.description) ?? "",
|
|
8618
|
+
app_id: getString(wf.app_id) ?? getString(wfApp.id) ?? "",
|
|
8619
|
+
app_name: getString(wfApp.name) ?? ""
|
|
8620
|
+
},
|
|
8621
|
+
collaborators: asArray(details.collaborators).map((c) => typeof c === "string" ? c : "").filter(Boolean)
|
|
8622
|
+
};
|
|
8623
|
+
}
|
|
8624
|
+
async function runWorkflow(client, input) {
|
|
8625
|
+
const clientToken = `cli-${Date.now()}`;
|
|
8626
|
+
const resp = await client.api("workflows.triggers.trip", {
|
|
8627
|
+
url: input.shortcutUrl,
|
|
8628
|
+
client_token: clientToken,
|
|
8629
|
+
context: JSON.stringify({
|
|
8630
|
+
location: "bookmark",
|
|
8631
|
+
channel_id: input.channelId,
|
|
8632
|
+
trigger_id: input.triggerId
|
|
8633
|
+
}),
|
|
8634
|
+
run_precheck: true
|
|
8635
|
+
});
|
|
8636
|
+
return {
|
|
8637
|
+
function_execution_id: getString(resp.function_execution_id) ?? "",
|
|
8638
|
+
trigger_execution_id: getString(resp.trigger_execution_id) ?? "",
|
|
8639
|
+
is_slow_workflow: resp.is_slow_workflow === true
|
|
8640
|
+
};
|
|
8641
|
+
}
|
|
8642
|
+
async function resolveShortcutUrl(client, input) {
|
|
8643
|
+
const { channelId, triggerId } = input;
|
|
8644
|
+
const resp = await client.api("bookmarks.list", {
|
|
8645
|
+
channel_id: channelId
|
|
8646
|
+
});
|
|
8647
|
+
const bookmarks = asArray(resp.bookmarks).filter(isRecord);
|
|
8648
|
+
for (const b of bookmarks) {
|
|
8649
|
+
const shortcutId = getString(b.shortcut_id);
|
|
8650
|
+
if (shortcutId === triggerId) {
|
|
8651
|
+
const link = getString(b.link);
|
|
8652
|
+
if (link) {
|
|
8653
|
+
return link;
|
|
8654
|
+
}
|
|
8655
|
+
}
|
|
8656
|
+
}
|
|
8657
|
+
throw new Error(`Could not find shortcut URL for trigger ${triggerId} in channel bookmarks`);
|
|
8658
|
+
}
|
|
8659
|
+
async function getWorkflowSchema(client, workflowId) {
|
|
8660
|
+
const resp = await client.api("workflows.get", { workflow_id: workflowId });
|
|
8661
|
+
const wf = isRecord(resp.workflow) ? resp.workflow : null;
|
|
8662
|
+
if (!wf) {
|
|
8663
|
+
throw new Error(`No workflow found for ID ${workflowId}`);
|
|
8664
|
+
}
|
|
8665
|
+
const steps = asArray(wf.steps).filter(isRecord);
|
|
8666
|
+
const stepSummaries = [];
|
|
8667
|
+
let fields = [];
|
|
8668
|
+
let formTitle;
|
|
8669
|
+
for (const step of steps) {
|
|
8670
|
+
const fn = isRecord(step.function) ? step.function : {};
|
|
8671
|
+
const callbackId = getString(fn.callback_id) ?? "";
|
|
8672
|
+
const title = getString(fn.title) ?? callbackId;
|
|
8673
|
+
stepSummaries.push(title);
|
|
8674
|
+
if (callbackId === "open_form") {
|
|
8675
|
+
const inputs = isRecord(step.inputs) ? step.inputs : {};
|
|
8676
|
+
const titleInput = isRecord(inputs.title) ? inputs.title : {};
|
|
8677
|
+
formTitle = getString(titleInput.value);
|
|
8678
|
+
const fieldsInput = isRecord(inputs.fields) ? inputs.fields : {};
|
|
8679
|
+
const fieldsValue = isRecord(fieldsInput.value) ? fieldsInput.value : {};
|
|
8680
|
+
const elements = asArray(fieldsValue.elements).filter(isRecord);
|
|
8681
|
+
const required = new Set(asArray(fieldsValue.required).map((r) => typeof r === "string" ? r : "").filter(Boolean));
|
|
8682
|
+
fields = elements.map((el) => ({
|
|
8683
|
+
name: getString(el.name) ?? "",
|
|
8684
|
+
title: getString(el.title) ?? "",
|
|
8685
|
+
type: getString(el.type) ?? "string",
|
|
8686
|
+
description: getString(el.description) ?? "",
|
|
8687
|
+
required: required.has(getString(el.name) ?? ""),
|
|
8688
|
+
long: el.long === true ? true : undefined
|
|
8689
|
+
}));
|
|
8690
|
+
}
|
|
8691
|
+
}
|
|
8692
|
+
return {
|
|
8693
|
+
workflow_id: getString(wf.id) ?? workflowId,
|
|
8694
|
+
title: getString(wf.title) ?? "",
|
|
8695
|
+
description: getString(wf.description) ?? "",
|
|
8696
|
+
form_title: formTitle,
|
|
8697
|
+
fields,
|
|
8698
|
+
steps: stepSummaries
|
|
8699
|
+
};
|
|
8700
|
+
}
|
|
8701
|
+
function extractTriggerId(link) {
|
|
8702
|
+
if (!link) {
|
|
8703
|
+
return;
|
|
8704
|
+
}
|
|
8705
|
+
const match = link.match(/slack\.com\/shortcuts\/(Ft[A-Za-z0-9]+)/);
|
|
8706
|
+
return match?.[1];
|
|
8707
|
+
}
|
|
8708
|
+
|
|
8709
|
+
// src/cli/workflow-command.ts
|
|
8710
|
+
function registerWorkflowCommand(input) {
|
|
8711
|
+
const workflowCmd = input.program.command("workflow").description("Discover and interact with Slack workflows");
|
|
8712
|
+
workflowCmd.command("list").description("List workflows bookmarked or featured in a channel").argument("<channel>", "Channel id or name (#channel, channel, C...)").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if you have multiple workspaces)").action(async (...args) => {
|
|
8713
|
+
const [channel, options] = args;
|
|
8714
|
+
try {
|
|
8715
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
|
|
8716
|
+
const payload = await input.ctx.withAutoRefresh({
|
|
8717
|
+
workspaceUrl,
|
|
8718
|
+
work: async () => {
|
|
8719
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
8720
|
+
const channelId = await resolveChannelId(client, channel);
|
|
8721
|
+
return await listChannelWorkflows(client, channelId);
|
|
8722
|
+
}
|
|
8723
|
+
});
|
|
8724
|
+
console.log(JSON.stringify(pruneEmpty(payload), null, 2));
|
|
8725
|
+
} catch (err) {
|
|
8726
|
+
console.error(input.ctx.errorMessage(err));
|
|
8727
|
+
process.exitCode = 1;
|
|
8728
|
+
}
|
|
8729
|
+
});
|
|
8730
|
+
workflowCmd.command("preview").description("Get workflow metadata from a trigger ID (no side effects)").argument("<trigger-id>", "Trigger ID (Ft...)").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if you have multiple workspaces)").action(async (...args) => {
|
|
8731
|
+
const [triggerId, options] = args;
|
|
8732
|
+
try {
|
|
8733
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
|
|
8734
|
+
const payload = await input.ctx.withAutoRefresh({
|
|
8735
|
+
workspaceUrl,
|
|
8736
|
+
work: async () => {
|
|
8737
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
8738
|
+
return await previewWorkflow(client, triggerId);
|
|
8739
|
+
}
|
|
8740
|
+
});
|
|
8741
|
+
console.log(JSON.stringify(pruneEmpty(payload), null, 2));
|
|
8742
|
+
} catch (err) {
|
|
8743
|
+
console.error(input.ctx.errorMessage(err));
|
|
8744
|
+
process.exitCode = 1;
|
|
8745
|
+
}
|
|
8746
|
+
});
|
|
8747
|
+
workflowCmd.command("get").description("Get workflow definition including form fields and steps (accepts Ft... or Wf...)").argument("<id>", "Trigger ID (Ft...) or Workflow ID (Wf...)").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if you have multiple workspaces)").action(async (...args) => {
|
|
8748
|
+
const [id, options] = args;
|
|
8749
|
+
try {
|
|
8750
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
|
|
8751
|
+
const payload = await input.ctx.withAutoRefresh({
|
|
8752
|
+
workspaceUrl,
|
|
8753
|
+
work: async () => {
|
|
8754
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
8755
|
+
let workflowId = id;
|
|
8756
|
+
if (id.startsWith("Ft")) {
|
|
8757
|
+
const preview = await previewWorkflow(client, id);
|
|
8758
|
+
workflowId = preview.workflow.id;
|
|
8759
|
+
}
|
|
8760
|
+
return await getWorkflowSchema(client, workflowId);
|
|
8761
|
+
}
|
|
8762
|
+
});
|
|
8763
|
+
console.log(JSON.stringify(pruneEmpty(payload), null, 2));
|
|
8764
|
+
} catch (err) {
|
|
8765
|
+
console.error(input.ctx.errorMessage(err));
|
|
8766
|
+
process.exitCode = 1;
|
|
8767
|
+
}
|
|
8768
|
+
});
|
|
8769
|
+
workflowCmd.command("run").description("Trip a workflow trigger").argument("<trigger-id>", "Trigger ID (Ft...)").requiredOption("--channel <id-or-name>", "Channel where the workflow is bookmarked").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if you have multiple workspaces)").action(async (...args) => {
|
|
8770
|
+
const [triggerId, options] = args;
|
|
8771
|
+
try {
|
|
8772
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
|
|
8773
|
+
const payload = await input.ctx.withAutoRefresh({
|
|
8774
|
+
workspaceUrl,
|
|
8775
|
+
work: async () => {
|
|
8776
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
8777
|
+
const channelId = await resolveChannelId(client, options.channel);
|
|
8778
|
+
const shortcutUrl = await resolveShortcutUrl(client, { channelId, triggerId });
|
|
8779
|
+
return await runWorkflow(client, { shortcutUrl, channelId, triggerId });
|
|
8780
|
+
}
|
|
8781
|
+
});
|
|
8782
|
+
console.log(JSON.stringify(pruneEmpty(payload), null, 2));
|
|
8783
|
+
} catch (err) {
|
|
8784
|
+
console.error(input.ctx.errorMessage(err));
|
|
8785
|
+
process.exitCode = 1;
|
|
8786
|
+
}
|
|
8787
|
+
});
|
|
8788
|
+
}
|
|
8789
|
+
|
|
7831
8790
|
// src/index.ts
|
|
7832
8791
|
var program = new Command;
|
|
7833
8792
|
program.name("agent-slack").description("Slack automation CLI for AI agents").version(getPackageVersion());
|
|
@@ -7836,9 +8795,12 @@ registerAuthCommand({ program, ctx });
|
|
|
7836
8795
|
registerMessageCommand({ program, ctx });
|
|
7837
8796
|
registerCanvasCommand({ program, ctx });
|
|
7838
8797
|
registerSearchCommand({ program, ctx });
|
|
8798
|
+
registerLaterCommand({ program, ctx });
|
|
8799
|
+
registerUnreadsCommand({ program, ctx });
|
|
7839
8800
|
registerUpdateCommand({ program });
|
|
7840
8801
|
registerUserCommand({ program, ctx });
|
|
7841
8802
|
registerChannelCommand({ program, ctx });
|
|
8803
|
+
registerWorkflowCommand({ program, ctx });
|
|
7842
8804
|
program.parse(process.argv);
|
|
7843
8805
|
if (!process.argv.slice(2).length) {
|
|
7844
8806
|
program.outputHelp();
|
|
@@ -7848,5 +8810,5 @@ if (subcommand && subcommand !== "update") {
|
|
|
7848
8810
|
backgroundUpdateCheck();
|
|
7849
8811
|
}
|
|
7850
8812
|
|
|
7851
|
-
//# debugId=
|
|
8813
|
+
//# debugId=517A87768FFFD43D64756E2164756E21
|
|
7852
8814
|
//# sourceMappingURL=index.js.map
|