agent-slack 0.6.1 → 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) {
@@ -7171,6 +7206,569 @@ function registerSearchCommand(input) {
7171
7206
  create({ kind: "files", name: "files", desc: "Search files" });
7172
7207
  }
7173
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
+
7174
7772
  // src/lib/update.ts
7175
7773
  import { execSync as execSync2 } from "node:child_process";
7176
7774
  import { createHash } from "node:crypto";
@@ -7921,6 +8519,274 @@ function registerChannelCommand(input) {
7921
8519
  });
7922
8520
  }
7923
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
+
7924
8790
  // src/index.ts
7925
8791
  var program = new Command;
7926
8792
  program.name("agent-slack").description("Slack automation CLI for AI agents").version(getPackageVersion());
@@ -7929,9 +8795,12 @@ registerAuthCommand({ program, ctx });
7929
8795
  registerMessageCommand({ program, ctx });
7930
8796
  registerCanvasCommand({ program, ctx });
7931
8797
  registerSearchCommand({ program, ctx });
8798
+ registerLaterCommand({ program, ctx });
8799
+ registerUnreadsCommand({ program, ctx });
7932
8800
  registerUpdateCommand({ program });
7933
8801
  registerUserCommand({ program, ctx });
7934
8802
  registerChannelCommand({ program, ctx });
8803
+ registerWorkflowCommand({ program, ctx });
7935
8804
  program.parse(process.argv);
7936
8805
  if (!process.argv.slice(2).length) {
7937
8806
  program.outputHelp();
@@ -7941,5 +8810,5 @@ if (subcommand && subcommand !== "update") {
7941
8810
  backgroundUpdateCheck();
7942
8811
  }
7943
8812
 
7944
- //# debugId=3EBC31FAC3ADA2FD64756E2164756E21
8813
+ //# debugId=517A87768FFFD43D64756E2164756E21
7945
8814
  //# sourceMappingURL=index.js.map