agent-slack 0.8.5 → 0.9.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 CHANGED
@@ -231,6 +231,8 @@ Attach options for `message send`:
231
231
 
232
232
  - `--attach <path>` upload a local file (repeatable)
233
233
 
234
+ `message send` returns `channel_id` plus the posted `ts` and a `permalink` (for non-attachment sends). `thread_ts` appears only when replying in a thread.
235
+
234
236
  ### List, create, and invite channels
235
237
 
236
238
  ```bash
@@ -335,6 +337,81 @@ agent-slack user get U12345678 --workspace "https://workspace.slack.com" | jq .
335
337
  agent-slack user get "@alice" --workspace "https://workspace.slack.com" | jq .
336
338
  ```
337
339
 
340
+ ### Unreads (inbox view)
341
+
342
+ See all unread messages across channels, DMs, and threads in one place:
343
+
344
+ ```bash
345
+ # Show all unreads with message content
346
+ agent-slack unreads
347
+
348
+ # Show only unread counts (no message content)
349
+ agent-slack unreads --counts-only
350
+
351
+ # Limit messages per channel (default 10)
352
+ agent-slack unreads --max-messages 5
353
+
354
+ # Include system messages (joins, leaves, topic changes)
355
+ agent-slack unreads --include-system
356
+ ```
357
+
358
+ Output includes channels sorted by mention count, then unread count:
359
+
360
+ ```json
361
+ {
362
+ "channels": [
363
+ {
364
+ "channel_id": "C123...",
365
+ "channel_name": "general",
366
+ "channel_type": "channel",
367
+ "unread_count": 5,
368
+ "mention_count": 2,
369
+ "messages": [...]
370
+ }
371
+ ],
372
+ "threads": {
373
+ "has_unreads": true,
374
+ "mention_count": 3
375
+ }
376
+ }
377
+ ```
378
+
379
+ Note: This feature uses the `client.counts` API which may be restricted in some Enterprise Grid workspaces (`team_is_restricted` error).
380
+
381
+ ### Later (saved messages)
382
+
383
+ Manage your saved-for-later messages (Slack's Later tab):
384
+
385
+ ```bash
386
+ # List saved messages (in-progress by default)
387
+ agent-slack later list
388
+
389
+ # Show only counts per state
390
+ agent-slack later list --counts-only
391
+
392
+ # Filter by state: in_progress, completed, archived, all
393
+ agent-slack later list --state completed
394
+
395
+ # Save a message for later
396
+ agent-slack later save "https://workspace.slack.com/archives/C123/p1700000000000000"
397
+
398
+ # Mark as completed
399
+ agent-slack later complete "https://workspace.slack.com/archives/C123/p1700000000000000"
400
+
401
+ # Archive
402
+ agent-slack later archive "https://workspace.slack.com/archives/C123/p1700000000000000"
403
+
404
+ # Move back to in-progress
405
+ agent-slack later reopen "https://workspace.slack.com/archives/C123/p1700000000000000"
406
+
407
+ # Remove from saved
408
+ agent-slack later remove "https://workspace.slack.com/archives/C123/p1700000000000000"
409
+
410
+ # Set a reminder
411
+ agent-slack later remind "https://workspace.slack.com/archives/C123/p1700000000000000" --in 1h
412
+ agent-slack later remind "https://workspace.slack.com/archives/C123/p1700000000000000" --in tomorrow
413
+ ```
414
+
338
415
  ### Fetch a Canvas as Markdown
339
416
 
340
417
  ```bash
package/dist/index.js CHANGED
@@ -3541,6 +3541,16 @@ function parseSlackMessageUrl(input) {
3541
3541
  const workspace_url = `${url.protocol}//${url.host}`;
3542
3542
  return { workspace_url, channel_id, message_ts, thread_ts_hint, raw: input, possiblyTruncated };
3543
3543
  }
3544
+ function buildSlackMessageUrl(input) {
3545
+ const workspaceUrl = input.workspace_url.replace(/\/$/, "");
3546
+ const digits = input.message_ts.replace(".", "");
3547
+ const url = new URL(`${workspaceUrl}/archives/${input.channel_id}/p${digits}`);
3548
+ if (input.thread_ts && input.thread_ts !== input.message_ts) {
3549
+ url.searchParams.set("thread_ts", input.thread_ts);
3550
+ url.searchParams.set("cid", input.channel_id);
3551
+ }
3552
+ return url.toString();
3553
+ }
3544
3554
 
3545
3555
  // src/cli/targets.ts
3546
3556
  function parseMsgTarget(input) {
@@ -3579,14 +3589,32 @@ var CODE_BLOCK_START = /^```/;
3579
3589
  var BLOCKQUOTE_RE = /^> (.*)$/;
3580
3590
  function parseInlineElements(text) {
3581
3591
  const elements = [];
3582
- const re = /`([^`]+)`|\*([^*]+)\*|_([^_]+)_|~([^~]+)~|<([^>|]+)\|([^>]+)>|<([^>|]+)>/g;
3592
+ const re = /`([^`]+)`|\*([^*]+)\*|_([^_]+)_|~([^~]+)~|<@([UWB][A-Z0-9]+)(?:\|[^>]*)?>|<!(here|channel|everyone)(?:\|[^>]*)?>|<([^>|]+)\|([^>]+)>|<([^>|]+)>|(?:^|(?<=[^A-Za-z0-9_]))@([UWB][A-Z0-9]{6,})\b|(?:^|(?<=[^A-Za-z0-9_]))@(here|channel|everyone)\b/g;
3583
3593
  let lastIndex = 0;
3584
3594
  let match;
3595
+ const pushText = (slice) => {
3596
+ if (slice) {
3597
+ elements.push({ type: "text", text: slice });
3598
+ }
3599
+ };
3585
3600
  while ((match = re.exec(text)) !== null) {
3586
3601
  if (match.index > lastIndex) {
3587
- elements.push({ type: "text", text: text.slice(lastIndex, match.index) });
3588
- }
3589
- const [, code, bold, italic, strike, linkUrl, linkText, bareUrl] = match;
3602
+ pushText(text.slice(lastIndex, match.index));
3603
+ }
3604
+ const [
3605
+ ,
3606
+ code,
3607
+ bold,
3608
+ italic,
3609
+ strike,
3610
+ userToken,
3611
+ broadcastToken,
3612
+ linkUrl,
3613
+ linkText,
3614
+ bareUrl,
3615
+ bareUserId,
3616
+ bareBroadcast
3617
+ ] = match;
3590
3618
  if (code != null) {
3591
3619
  elements.push({ type: "text", text: code, style: { code: true } });
3592
3620
  } else if (bold != null) {
@@ -3595,15 +3623,29 @@ function parseInlineElements(text) {
3595
3623
  elements.push({ type: "text", text: italic, style: { italic: true } });
3596
3624
  } else if (strike != null) {
3597
3625
  elements.push({ type: "text", text: strike, style: { strike: true } });
3626
+ } else if (userToken != null) {
3627
+ elements.push({ type: "user", user_id: userToken });
3628
+ } else if (broadcastToken != null) {
3629
+ elements.push({
3630
+ type: "broadcast",
3631
+ range: broadcastToken
3632
+ });
3598
3633
  } else if (linkUrl != null && linkText != null) {
3599
3634
  elements.push({ type: "link", url: linkUrl, text: linkText });
3600
3635
  } else if (bareUrl != null) {
3601
3636
  elements.push({ type: "link", url: bareUrl });
3637
+ } else if (bareUserId != null) {
3638
+ elements.push({ type: "user", user_id: bareUserId });
3639
+ } else if (bareBroadcast != null) {
3640
+ elements.push({
3641
+ type: "broadcast",
3642
+ range: bareBroadcast
3643
+ });
3602
3644
  }
3603
3645
  lastIndex = match.index + match[0].length;
3604
3646
  }
3605
3647
  if (lastIndex < text.length) {
3606
- elements.push({ type: "text", text: text.slice(lastIndex) });
3648
+ pushText(text.slice(lastIndex));
3607
3649
  }
3608
3650
  return elements.length > 0 ? elements : [{ type: "text", text }];
3609
3651
  }
@@ -3726,6 +3768,23 @@ function collectList(input) {
3726
3768
  return idx;
3727
3769
  }
3728
3770
 
3771
+ // src/slack/format-outbound.ts
3772
+ function formatOutboundSlackText(text) {
3773
+ if (!text) {
3774
+ return "";
3775
+ }
3776
+ const stash = [];
3777
+ let out = text.replace(/<(?:@[UWB][A-Z0-9]+(?:\|[^>]*)?|#[CG][A-Z0-9]+(?:\|[^>]*)?|![a-zA-Z]+(?:\|[^>]*)?|https?:\/\/[^>]+)>/g, (m) => {
3778
+ stash.push(m);
3779
+ return `\x00${stash.length - 1}\x00`;
3780
+ });
3781
+ out = out.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3782
+ out = out.replace(/(^|[^A-Za-z0-9_])@([UWB][A-Z0-9]{6,})\b/g, (_m, pre, id) => `${pre}<@${id}>`);
3783
+ out = out.replace(/(^|[^A-Za-z0-9_])@(here|channel|everyone)\b/g, (_m, pre, name) => `${pre}<!${name}>`);
3784
+ out = out.replace(/\u0000(\d+)\u0000/g, (_m, idx) => stash[Number(idx)]);
3785
+ return out;
3786
+ }
3787
+
3729
3788
  // src/slack/upload.ts
3730
3789
  import { readFile as readFile5, stat, realpath } from "node:fs/promises";
3731
3790
  import { basename as basename2 } from "node:path";
@@ -4585,68 +4644,69 @@ function requireOldestWhenReactionFiltersUsed(input) {
4585
4644
  }
4586
4645
  async function sendMessage(input) {
4587
4646
  const target = parseMsgTarget(String(input.targetInput));
4647
+ const formattedText = formatOutboundSlackText(input.text);
4588
4648
  const blocks = input.text ? textToRichTextBlocks(input.text) : null;
4589
4649
  const attachPaths = normalizeAttachPaths(input.options.attach);
4590
4650
  if (target.kind === "url") {
4591
4651
  const { ref } = target;
4592
4652
  warnOnTruncatedSlackUrl(ref);
4593
- await input.ctx.withAutoRefresh({
4653
+ return await input.ctx.withAutoRefresh({
4594
4654
  workspaceUrl: ref.workspace_url,
4595
4655
  work: async () => {
4596
- const { client } = await input.ctx.getClientForWorkspace(ref.workspace_url);
4656
+ const { client, workspace_url } = await input.ctx.getClientForWorkspace(ref.workspace_url);
4597
4657
  const msg = await fetchMessage(client, { ref });
4598
4658
  const threadTs = msg.thread_ts ?? msg.ts;
4599
- await sendMessageToChannel({
4659
+ return await sendMessageToChannel({
4600
4660
  client,
4661
+ workspaceUrl: workspace_url ?? ref.workspace_url,
4601
4662
  channelId: ref.channel_id,
4602
- text: input.text,
4663
+ text: formattedText,
4603
4664
  blocks,
4604
4665
  threadTs,
4605
4666
  attachPaths
4606
4667
  });
4607
4668
  }
4608
4669
  });
4609
- return { ok: true };
4610
4670
  }
4611
4671
  if (target.kind === "user") {
4612
4672
  const workspaceUrl2 = input.ctx.effectiveWorkspaceUrl(input.options.workspace);
4613
- await input.ctx.withAutoRefresh({
4673
+ return await input.ctx.withAutoRefresh({
4614
4674
  workspaceUrl: workspaceUrl2,
4615
4675
  work: async () => {
4616
- const { client } = await input.ctx.getClientForWorkspace(workspaceUrl2);
4676
+ const { client, workspace_url } = await input.ctx.getClientForWorkspace(workspaceUrl2);
4617
4677
  const dmChannelId = await openDmChannel(client, target.userId);
4618
- await sendMessageToChannel({
4678
+ return await sendMessageToChannel({
4619
4679
  client,
4680
+ workspaceUrl: workspace_url ?? workspaceUrl2,
4620
4681
  channelId: dmChannelId,
4621
- text: input.text,
4682
+ text: formattedText,
4622
4683
  blocks,
4623
4684
  attachPaths
4624
4685
  });
4625
4686
  }
4626
4687
  });
4627
- return { ok: true };
4628
4688
  }
4629
4689
  const workspaceUrl = input.ctx.effectiveWorkspaceUrl(input.options.workspace);
4630
4690
  await input.ctx.assertWorkspaceSpecifiedForChannelNames({
4631
4691
  workspaceUrl,
4632
4692
  channels: [String(target.channel)]
4633
4693
  });
4634
- await input.ctx.withAutoRefresh({
4694
+ return await input.ctx.withAutoRefresh({
4635
4695
  workspaceUrl,
4636
4696
  work: async () => {
4637
- const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
4697
+ const { client, workspace_url } = await input.ctx.getClientForWorkspace(workspaceUrl);
4638
4698
  const channelId = await resolveChannelId(client, String(target.channel));
4639
- await sendMessageToChannel({
4699
+ return await sendMessageToChannel({
4640
4700
  client,
4701
+ workspaceUrl: workspace_url ?? workspaceUrl,
4641
4702
  channelId,
4642
- text: input.text,
4703
+ text: formattedText,
4643
4704
  blocks,
4644
4705
  threadTs: input.options.threadTs ? String(input.options.threadTs) : undefined,
4645
4706
  attachPaths
4646
4707
  });
4647
4708
  }
4648
4709
  });
4649
- return { ok: true };
4650
4710
  }
4651
4711
  function normalizeAttachPaths(raw) {
4652
4712
  if (!Array.isArray(raw) || raw.length === 0) {
@@ -4662,13 +4722,27 @@ function normalizeAttachPaths(raw) {
4662
4722
  }
4663
4723
  async function sendMessageToChannel(input) {
4664
4724
  if (input.attachPaths.length === 0) {
4665
- await input.client.api("chat.postMessage", {
4725
+ const resp = await input.client.api("chat.postMessage", {
4666
4726
  channel: input.channelId,
4667
4727
  text: input.text,
4668
4728
  thread_ts: input.threadTs,
4669
4729
  ...input.blocks ? { blocks: input.blocks } : {}
4670
4730
  });
4671
- return;
4731
+ const ts = typeof resp.ts === "string" ? resp.ts : undefined;
4732
+ const channelId = typeof resp.channel === "string" ? resp.channel : input.channelId;
4733
+ const permalink = input.workspaceUrl && ts ? buildSlackMessageUrl({
4734
+ workspace_url: input.workspaceUrl,
4735
+ channel_id: channelId,
4736
+ message_ts: ts,
4737
+ thread_ts: input.threadTs
4738
+ }) : undefined;
4739
+ return {
4740
+ ok: true,
4741
+ channel_id: channelId,
4742
+ ts,
4743
+ thread_ts: input.threadTs,
4744
+ permalink
4745
+ };
4672
4746
  }
4673
4747
  if (input.blocks) {
4674
4748
  process.stderr.write(`Warning: rich text formatting is not supported with file attachments; sending as plain text.
@@ -4685,6 +4759,11 @@ async function sendMessageToChannel(input) {
4685
4759
  });
4686
4760
  initialComment = "";
4687
4761
  }
4762
+ return {
4763
+ ok: true,
4764
+ channel_id: input.channelId,
4765
+ thread_ts: input.threadTs
4766
+ };
4688
4767
  }
4689
4768
  async function editMessage(input) {
4690
4769
  const target = parseMsgTarget(String(input.targetInput));
@@ -4692,6 +4771,7 @@ async function editMessage(input) {
4692
4771
  throw new Error("message edit does not support user ID targets. Use a channel name, channel ID, or message URL.");
4693
4772
  }
4694
4773
  const workspaceUrl = input.ctx.effectiveWorkspaceUrl(input.options.workspace);
4774
+ const formattedText = formatOutboundSlackText(input.text);
4695
4775
  await input.ctx.withAutoRefresh({
4696
4776
  workspaceUrl: target.kind === "url" ? target.ref.workspace_url : workspaceUrl,
4697
4777
  work: async () => {
@@ -4702,7 +4782,7 @@ async function editMessage(input) {
4702
4782
  await client2.api("chat.update", {
4703
4783
  channel: ref.channel_id,
4704
4784
  ts: ref.message_ts,
4705
- text: input.text
4785
+ text: formattedText
4706
4786
  });
4707
4787
  return;
4708
4788
  }
@@ -4716,7 +4796,7 @@ async function editMessage(input) {
4716
4796
  await client.api("chat.update", {
4717
4797
  channel: channelId,
4718
4798
  ts,
4719
- text: input.text
4799
+ text: formattedText
4720
4800
  });
4721
4801
  }
4722
4802
  });
@@ -9156,5 +9236,5 @@ if (subcommand && subcommand !== "update") {
9156
9236
  backgroundUpdateCheck();
9157
9237
  }
9158
9238
 
9159
- //# debugId=DD5B850D326A863A64756E2164756E21
9239
+ //# debugId=D4BE12D81E739FC964756E2164756E21
9160
9240
  //# sourceMappingURL=index.js.map