slack-term 1.52.0 → 1.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-term",
3
- "version": "1.52.0",
3
+ "version": "1.53.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/slack-term.git"
package/ts/cli.ts CHANGED
@@ -9,7 +9,7 @@ import { dirname, join, resolve } from "node:path";
9
9
 
10
10
  import yargs, { type Options } from "yargs";
11
11
  import { hideBin } from "yargs/helpers";
12
- import { guardUrlBoundaries } from "./urlGuard.ts";
12
+ import { fixUrlBoundaries, guardUrlBoundaries } from "./urlGuard.ts";
13
13
  import { listProfiles, removeProfile, resolveBotToken, resolveCookie, resolveToken, useProfile, type Profile } from "./profiles.ts";
14
14
  import { diagnoseBotMessaging, formatDiagnosis } from "./botdoctor.ts";
15
15
  import { cmdAuthLogin, cmdAuthLoginChrome, cmdAuthChrome, cmdAuthFirefox, cmdAuthToken, cmdAuthApp, cmdAuthSave, cmdAuthTokens } from "./auth.ts";
@@ -5825,7 +5825,7 @@ async function main(): Promise<void> {
5825
5825
  * prefix and thread check first). */
5826
5826
  type SendCli = {
5827
5827
  workspace?: string | undefined; target?: string | undefined; message?: string | undefined; code?: string | undefined;
5828
- "allow-url-adjacent"?: boolean | undefined; "channel-id"?: string | undefined; "user-id"?: string | undefined;
5828
+ "allow-url-adjacent"?: boolean | undefined; "fix-urls"?: boolean | undefined; "channel-id"?: string | undefined; "user-id"?: string | undefined;
5829
5829
  "as-bot"?: boolean | undefined; broadcast?: boolean | undefined; mentions?: boolean | undefined;
5830
5830
  json?: boolean | undefined;
5831
5831
  };
@@ -5834,7 +5834,7 @@ async function main(): Promise<void> {
5834
5834
  * its subscriptions. */
5835
5835
  type EditCli = {
5836
5836
  workspace?: string | undefined; target?: string | undefined; newText?: string | undefined; code?: string | undefined;
5837
- force?: boolean | undefined; "allow-url-adjacent"?: boolean | undefined; "channel-id"?: string | undefined;
5837
+ force?: boolean | undefined; "allow-url-adjacent"?: boolean | undefined; "fix-urls"?: boolean | undefined; "channel-id"?: string | undefined;
5838
5838
  mentions?: boolean | undefined; "as-bot"?: boolean | undefined; tick?: { n?: number } | undefined;
5839
5839
  };
5840
5840
  const runEdit = async (argv: EditCli): Promise<void> => {
@@ -5852,6 +5852,7 @@ async function main(): Promise<void> {
5852
5852
  process.exit(1);
5853
5853
  }
5854
5854
  }
5855
+ if (argv["fix-urls"] && argv.newText) argv = { ...argv, newText: fixUrlBoundaries(argv.newText) };
5855
5856
  const args: EditArgs = { target: argv.target!, newText: argv.newText ?? "", allowUrlAdjacent: argv["allow-url-adjacent"] === true };
5856
5857
  if (argv.tick) args.tick = argv.tick;
5857
5858
  if (settings.prefix) args.lanePrefix = settings.prefix;
@@ -5898,6 +5899,7 @@ async function main(): Promise<void> {
5898
5899
  console.error("Error: message is empty");
5899
5900
  process.exit(1);
5900
5901
  }
5902
+ if (argv["fix-urls"]) argv = { ...argv, message: fixUrlBoundaries(argv.message!) };
5901
5903
  const args: SendArgs = { target: argv.target!, message: argv.message!, allowUrlAdjacent: argv["allow-url-adjacent"] === true };
5902
5904
  if (argv.json) args.json = true;
5903
5905
  if (argv.code) args.code = argv.code;
@@ -6546,6 +6548,7 @@ async function main(): Promise<void> {
6546
6548
  .positional("message", { type: "string", demandOption: true })
6547
6549
  .option("code", { type: "string", describe: "Safety hash to confirm send" })
6548
6550
  .option("allow-url-adjacent", { type: "boolean", default: false, describe: "Warn instead of refusing ambiguous bare URL boundaries" })
6551
+ .option("fix-urls", { type: "boolean", default: false, describe: "Wrap ambiguous bare URLs as <url> (cut at the first non-ASCII character / trailing punctuation) before sending" })
6549
6552
  .option("channel-id", { type: "string", describe: "Raw channel ID" })
6550
6553
  .option("user-id", { type: "string", describe: "Raw user ID (opens DM)" })
6551
6554
  .option("as-bot", { type: "boolean", default: false, describe: "Send via the bot token (xoxb / SLACK_BOT_TOKEN) so a DM notifies the recipient and can be two-way" })
@@ -6724,6 +6727,7 @@ async function main(): Promise<void> {
6724
6727
  .option("code", { type: "string", describe: "Safety hash to confirm send" })
6725
6728
  .option("as-bot", { type: "boolean", describe: "Send via the bot token (default: settings' as_bot)" })
6726
6729
  .option("allow-url-adjacent", { type: "boolean", default: false, describe: "Warn instead of refusing ambiguous bare URL boundaries" })
6730
+ .option("fix-urls", { type: "boolean", default: false, describe: "Wrap ambiguous bare URLs as <url> (cut at the first non-ASCII character / trailing punctuation) before sending" })
6727
6731
  .option("broadcast", { type: "boolean", default: false, describe: "Also send to channel" })
6728
6732
  .option("mentions", { type: "boolean", default: true, describe: "Convert @handle tokens to real mentions (--no-mentions for literal text)" }),
6729
6733
  async (argv) => {
@@ -6758,7 +6762,7 @@ async function main(): Promise<void> {
6758
6762
  if (!text.trim()) { console.error("Error: message is empty (only the prefix)"); process.exit(1); }
6759
6763
  await runSend({
6760
6764
  workspace: (argv as W).workspace, target: `${channel}:${threadTs}`, message: text, code: argv.code,
6761
- "allow-url-adjacent": argv["allow-url-adjacent"], "as-bot": argv["as-bot"] ?? settings.as_bot ?? false,
6765
+ "allow-url-adjacent": argv["allow-url-adjacent"], "fix-urls": argv["fix-urls"], "as-bot": argv["as-bot"] ?? settings.as_bot ?? false,
6762
6766
  broadcast: argv.broadcast, mentions: argv.mentions, json: argv.json,
6763
6767
  });
6764
6768
  },
@@ -7632,6 +7636,7 @@ async function main(): Promise<void> {
7632
7636
  .option("code", { type: "string", describe: "Safety hash to confirm edit" })
7633
7637
  .option("force", { type: "boolean", default: false, describe: "Apply an edit that BREAKS a `slack ask` / `slack poll` message (new text no longer reads as one, or an answered/void question made open again). Refused by default because collect reads that body back; edits that keep it readable (e.g. writing the ✅ form) need no flag. Prefer `slack ask --edit=` / `--void=`." })
7634
7638
  .option("allow-url-adjacent", { type: "boolean", default: false, describe: "Warn instead of refusing ambiguous bare URL boundaries" })
7639
+ .option("fix-urls", { type: "boolean", default: false, describe: "Wrap ambiguous bare URLs as <url> (cut at the first non-ASCII character / trailing punctuation) before sending" })
7635
7640
  .option("channel-id", { type: "string", describe: "Raw channel ID" })
7636
7641
  .option("mentions", { type: "boolean", default: true, describe: "Convert @handle tokens in the new text to real <@USERID> mentions (on by default). Unresolved handles stay as plain text. Disable with --no-mentions for literal @text." })
7637
7642
  .option("as-bot", { type: "boolean", default: false, describe: "Edit as the bot (xoxb / SLACK_BOT_TOKEN). REQUIRED to correct a message the bot posted: Slack only lets a token edit its OWN messages, so a bot-authored message is uneditable by the user token and chat.update returns cant_update_message. Address it by permalink or --channel-id; an @user target resolves against the BOT's DM list, which is where a bot-authored DM actually lives." }),
package/ts/urlGuard.ts CHANGED
@@ -16,7 +16,15 @@ const URL_TOKEN = /<https?:\/\/[^\s<>|]+(?:\|[^<>\r\n]*)?>|https?:\/\/[^\t\n\v\f
16
16
  /** What may sit right before a bare URL and still let it autolink. */
17
17
  const SAFE_BEFORE = /[\t\n\v\f\r (*_~]/;
18
18
 
19
- export type UrlBoundaryIssue = { url: string; side: "before" | "after"; char: string };
19
+ /** `trailing`: the URL ends in ASCII punctuation (`)` without a matching `(`, or `.,:;!?`) that Slack may or may
20
+ * not take into the link: `(see https://x/a)` would link `https://x/a)`. Only warned, never refused. */
21
+ export type UrlBoundaryIssue = { url: string; side: "before" | "after" | "trailing"; char: string };
22
+ const TRAILING = /[.,:;!?]$/;
23
+ function trailingChar(url: string): string | undefined {
24
+ if (TRAILING.test(url)) return url.slice(-1);
25
+ if (url.endsWith(")") && count(url, "(") < count(url, ")")) return ")";
26
+ return undefined;
27
+ }
20
28
 
21
29
  /** Code spans and fences are shown verbatim and never autolinked, so a URL in
22
30
  * one is not a boundary problem; neither is a `[label](url)` link, which is
@@ -97,6 +105,11 @@ export function urlBoundaryIssues(text: string, { markdown = true }: { markdown?
97
105
  }
98
106
  const after = url.match(/[^\x00-\x7f]|>/);
99
107
  if (after) issues.push({ url, side: "after", char: after[0] });
108
+ else {
109
+ // `(https://x/a)`: the `)` closes the paren before the URL, as GFM and Slack read it
110
+ const t = trailingChar(url);
111
+ if (t && !(t === ")" && before === "(")) issues.push({ url, side: "trailing", char: t });
112
+ }
100
113
  }
101
114
  return issues;
102
115
  }
@@ -107,22 +120,25 @@ export function adjacentUrls(text: string): string[] {
107
120
 
108
121
  export function guardUrlBoundaries(
109
122
  text: string, allow = false, warn: (message: string) => void = console.error,
110
- { sides = ["before", "after"], markdown = true }: {
123
+ { sides = ["before", "after", "trailing"], markdown = true }: {
111
124
  sides?: ReadonlyArray<UrlBoundaryIssue["side"]>; markdown?: boolean;
112
125
  } = {},
113
126
  ): void {
114
- const issues = urlBoundaryIssues(text, { markdown }).filter((i) => sides.includes(i.side));
127
+ const all = urlBoundaryIssues(text, { markdown });
128
+ const trailing = all.filter((i) => i.side === "trailing" && sides.includes("trailing"));
129
+ if (trailing.length) warn(`⚠ URL ends in punctuation Slack may take into the link: ${trailing.map((i) => `${JSON.stringify(i.url)} → ${JSON.stringify(fixUrlBoundaries(`x ${i.url}`).slice(2))}`).join(", ")}. If the ${JSON.stringify(trailing[0]!.char)} is not part of the URL, send the suggested form (or --fix-urls).`);
130
+ const issues = all.filter((i) => i.side !== "trailing" && sides.includes(i.side));
115
131
  if (!issues.length) return;
116
132
  const described = issues.map((i) => i.side === "before"
117
133
  ? `${JSON.stringify(i.url)} (glued to ${JSON.stringify(i.char)} before it — Slack will NOT make it a link)`
118
- : `${JSON.stringify(i.url)} (runs into ${JSON.stringify(i.char)} after it)`);
134
+ : `${JSON.stringify(i.url)} (runs into ${JSON.stringify(i.char)} after it; non-ASCII in a URL is OK only if intended, otherwise send ${JSON.stringify(fixUrlBoundaries(`x ${i.url}`).slice(2))} or put an ASCII space after the URL)`);
119
135
  const fixes = issues.some((i) => i.side === "before")
120
136
  ? "Put an ASCII space (or a newline) before the URL, " : "";
121
137
  const message = `Ambiguous URL boundary: ${described.join(", ")}. ` +
122
138
  `${fixes}Put the URL on its own line, or wrap it as <url> / <url|label>. ` +
123
- "Use --allow-url-adjacent to warn only.";
139
+ "Use --fix-urls to wrap them, or --allow-url-adjacent to warn only.";
124
140
  if (!allow) throw new Error(message);
125
- warn(`Warning: ${message}`);
141
+ warn(`⚠ Warning: ${message}`);
126
142
  }
127
143
 
128
144
  type RichTextElement =
@@ -167,6 +183,27 @@ export function linkifyRichText(text: string): RichTextElement[] {
167
183
  return out;
168
184
  }
169
185
 
186
+ /** Optional auto-fix (`--fix-urls`): wrap every ambiguous bare URL as `<url>`, cut where Slack's link would end
187
+ * (at the first non-ASCII character, before trailing punctuation). Text, code spans and explicit links are kept. */
188
+ export function fixUrlBoundaries(text: string): string {
189
+ const masked = maskCode(text);
190
+ let out = "", pos = 0;
191
+ for (const m of masked.matchAll(URL_TOKEN)) {
192
+ const at = m.index ?? 0;
193
+ if (m[0].startsWith("<")) continue;
194
+ let url = text.slice(at, at + m[0].length);
195
+ const cut = url.search(/[^\x00-\x7f]|>/);
196
+ if (cut >= 0) url = url.slice(0, cut);
197
+ while (trailingChar(url)) url = url.slice(0, -1);
198
+ const before = at > 0 ? masked[at - 1]! : "";
199
+ const ambiguous = url.length < m[0].length || (before && !SAFE_BEFORE.test(before));
200
+ if (!ambiguous || !/^https?:\/\/./i.test(url)) continue;
201
+ out += text.slice(pos, at) + `<${url}>`;
202
+ pos = at + url.length;
203
+ }
204
+ return out + text.slice(pos);
205
+ }
206
+
170
207
  function count(s: string, ch: string): number {
171
208
  return s.split(ch).length - 1;
172
209
  }