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/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
- const resp = await fetch(url, { headers });
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 Error(`Failed to download file (${resp.status})`);
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
- const text = await resp.text();
2445
- throw new Error(`Downloaded HTML instead of file (auth likely failed). First bytes: ${JSON.stringify(text.slice(0, 120))}`);
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
- const buf = Buffer.from(await resp.arrayBuffer());
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 path = input?.downloadedPaths?.[f.id];
3133
- if (!path) {
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 Error("Downloaded auth/login page instead of canvas content (token may be expired)");
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
- try {
3768
- if (isCanvas) {
3769
- downloadedPaths[file.id] = await downloadCanvasAsMarkdown({
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
- } else {
3776
- const ext = inferFileExtension(file);
3777
- downloadedPaths[file.id] = await downloadSlackFile({
3778
- auth: input.auth,
3779
- url,
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
- preferredName: `${file.id}${ext ? `.${ext}` : ""}`
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 path = await downloadSlackFile({
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 path = await downloadSlackFile({
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 path = await downloadSlackFile({
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
- input.downloadedPaths[f.id] = path;
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=5ADF75C59137BC5064756E2164756E21
8813
+ //# debugId=517A87768FFFD43D64756E2164756E21
7852
8814
  //# sourceMappingURL=index.js.map