agent-slack 0.2.13 → 0.3.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/dist/index.js CHANGED
@@ -47,9 +47,12 @@ function getUserAgent() {
47
47
  return `agent-slack/${getPackageVersion()}`;
48
48
  }
49
49
 
50
- // src/auth/chrome.ts
51
- import { execSync } from "node:child_process";
52
- import { platform } from "node:os";
50
+ // src/auth/brave.ts
51
+ import { execSync, execFileSync } from "node:child_process";
52
+ import { existsSync as existsSync2 } from "node:fs";
53
+ import { pbkdf2Sync, createDecipheriv } from "node:crypto";
54
+ import { homedir, platform } from "node:os";
55
+ import { join as join2 } from "node:path";
53
56
  var IS_MACOS = platform() === "darwin";
54
57
  function escapeOsaScript(script) {
55
58
  return script.replace(/'/g, `'"'"'`);
@@ -61,6 +64,205 @@ function osascript(script) {
61
64
  stdio: ["ignore", "pipe", "pipe"]
62
65
  }).trim();
63
66
  }
67
+ var TEAM_JSON_PATHS = [
68
+ "JSON.stringify(JSON.parse(localStorage.localConfig_v2).teams)",
69
+ "JSON.stringify(JSON.parse(localStorage.localConfig_v3).teams)",
70
+ "JSON.stringify(JSON.parse(localStorage.getItem('reduxPersist:localConfig'))?.teams || {})",
71
+ "JSON.stringify(window.boot_data?.teams || {})"
72
+ ];
73
+ function teamsScript() {
74
+ const tryPaths = TEAM_JSON_PATHS.map((expr) => `try { var v = ${expr}; if (v && v !== '{}' && v !== 'null') return v; } catch(e) {}`);
75
+ return `
76
+ tell application "Brave Browser"
77
+ repeat with w in windows
78
+ repeat with t in tabs of w
79
+ if URL of t contains "slack.com" then
80
+ return execute t javascript "(function(){ ${tryPaths.join(" ")} return '{}'; })()"
81
+ end if
82
+ end repeat
83
+ end repeat
84
+ return "{}"
85
+ end tell
86
+ `;
87
+ }
88
+ function isRecord(value) {
89
+ return typeof value === "object" && value !== null;
90
+ }
91
+ function toBraveTeam(value) {
92
+ if (!isRecord(value)) {
93
+ return null;
94
+ }
95
+ const token = typeof value.token === "string" ? value.token : null;
96
+ const url = typeof value.url === "string" ? value.url : null;
97
+ if (!token || !url || !token.startsWith("xoxc-")) {
98
+ return null;
99
+ }
100
+ const name = typeof value.name === "string" ? value.name : undefined;
101
+ return { url, name, token };
102
+ }
103
+ function extractTeamsFromBraveTab() {
104
+ const teamsRaw = osascript(teamsScript());
105
+ let teamsObj = {};
106
+ try {
107
+ teamsObj = JSON.parse(teamsRaw || "{}");
108
+ } catch {
109
+ teamsObj = {};
110
+ }
111
+ const teamsRecord = isRecord(teamsObj) ? teamsObj : {};
112
+ return Object.values(teamsRecord).map((t) => toBraveTeam(t)).filter((t) => t !== null);
113
+ }
114
+ var BRAVE_COOKIES_DB = join2(homedir(), "Library", "Application Support", "BraveSoftware", "Brave-Browser", "Default", "Cookies");
115
+ function isMissingBunSqliteModule(error) {
116
+ if (!error || typeof error !== "object") {
117
+ return false;
118
+ }
119
+ const err = error;
120
+ const code = typeof err.code === "string" ? err.code : "";
121
+ const message = typeof err.message === "string" ? err.message : "";
122
+ if (code === "ERR_MODULE_NOT_FOUND" || code === "ERR_UNSUPPORTED_ESM_URL_SCHEME") {
123
+ return true;
124
+ }
125
+ if (!message.includes("bun:sqlite")) {
126
+ return false;
127
+ }
128
+ return message.includes("Cannot find module") || message.includes("Unknown builtin module") || message.includes("unsupported URL scheme") || message.includes("Only URLs with a scheme in");
129
+ }
130
+ async function queryReadonlySqlite(dbPath, sql) {
131
+ try {
132
+ const { Database } = await import("bun:sqlite");
133
+ const db = new Database(dbPath, { readonly: true });
134
+ try {
135
+ return db.query(sql).all();
136
+ } finally {
137
+ db.close();
138
+ }
139
+ } catch (error) {
140
+ if (!isMissingBunSqliteModule(error)) {
141
+ throw error;
142
+ }
143
+ const { DatabaseSync } = await import("node:sqlite");
144
+ const db = new DatabaseSync(dbPath, { readOnly: true });
145
+ try {
146
+ return db.prepare(sql).all();
147
+ } finally {
148
+ db.close();
149
+ }
150
+ }
151
+ }
152
+ function getSafeStoragePasswords() {
153
+ const services = [
154
+ "Brave Safe Storage",
155
+ "Brave Browser Safe Storage",
156
+ "Chrome Safe Storage",
157
+ "Chromium Safe Storage"
158
+ ];
159
+ const passwords = [];
160
+ for (const service of services) {
161
+ try {
162
+ const out = execFileSync("security", ["find-generic-password", "-w", "-s", service], {
163
+ encoding: "utf8",
164
+ stdio: ["ignore", "pipe", "ignore"]
165
+ }).trim();
166
+ if (out) {
167
+ passwords.push(out);
168
+ }
169
+ } catch {}
170
+ }
171
+ return passwords;
172
+ }
173
+ function decryptChromiumCookieValue(data, password) {
174
+ if (!data || data.length === 0) {
175
+ return "";
176
+ }
177
+ const salt = Buffer.from("saltysalt", "utf8");
178
+ const iv = Buffer.alloc(16, " ");
179
+ const key = pbkdf2Sync(password, salt, 1003, 16, "sha1");
180
+ const decipher = createDecipheriv("aes-128-cbc", key, iv);
181
+ decipher.setAutoPadding(true);
182
+ const plain = Buffer.concat([decipher.update(data), decipher.final()]);
183
+ const marker = Buffer.from("xoxd-");
184
+ const idx = plain.indexOf(marker);
185
+ if (idx === -1) {
186
+ return plain.toString("utf8");
187
+ }
188
+ let end = idx;
189
+ while (end < plain.length) {
190
+ const b = plain[end];
191
+ if (b < 33 || b > 126) {
192
+ break;
193
+ }
194
+ end++;
195
+ }
196
+ const rawToken = plain.subarray(idx, end).toString("utf8");
197
+ try {
198
+ return decodeURIComponent(rawToken);
199
+ } catch {
200
+ return rawToken;
201
+ }
202
+ }
203
+ async function extractCookieDFromBrave() {
204
+ if (!existsSync2(BRAVE_COOKIES_DB)) {
205
+ throw new Error(`Brave Cookies DB not found: ${BRAVE_COOKIES_DB}`);
206
+ }
207
+ const rows = await queryReadonlySqlite(BRAVE_COOKIES_DB, "select host_key, name, value, encrypted_value from cookies where name = 'd' and host_key like '%slack.com' order by length(encrypted_value) desc");
208
+ if (!rows || rows.length === 0) {
209
+ throw new Error("No Slack 'd' cookie found in Brave");
210
+ }
211
+ const row = rows[0];
212
+ if (row.value && row.value.startsWith("xoxd-")) {
213
+ return row.value;
214
+ }
215
+ const encrypted = Buffer.from(row.encrypted_value || []);
216
+ if (encrypted.length === 0) {
217
+ throw new Error("Brave Slack 'd' cookie had no encrypted_value");
218
+ }
219
+ const prefix = encrypted.subarray(0, 3).toString("utf8");
220
+ const data = prefix === "v10" || prefix === "v11" ? encrypted.subarray(3) : encrypted;
221
+ const passwords = getSafeStoragePasswords();
222
+ for (const password of passwords) {
223
+ try {
224
+ const decrypted = decryptChromiumCookieValue(data, password);
225
+ const match = decrypted.match(/xoxd-[A-Za-z0-9%/+_=.-]+/);
226
+ if (match) {
227
+ return match[0];
228
+ }
229
+ } catch {}
230
+ }
231
+ throw new Error("Could not decrypt Slack 'd' cookie from Brave");
232
+ }
233
+ async function extractFromBrave() {
234
+ if (!IS_MACOS) {
235
+ return null;
236
+ }
237
+ try {
238
+ const teams = extractTeamsFromBraveTab();
239
+ if (teams.length === 0) {
240
+ return null;
241
+ }
242
+ const cookie_d = await extractCookieDFromBrave();
243
+ if (!cookie_d || !cookie_d.startsWith("xoxd-")) {
244
+ return null;
245
+ }
246
+ return { cookie_d, teams };
247
+ } catch {
248
+ return null;
249
+ }
250
+ }
251
+
252
+ // src/auth/chrome.ts
253
+ import { execSync as execSync2 } from "node:child_process";
254
+ import { platform as platform2 } from "node:os";
255
+ var IS_MACOS2 = platform2() === "darwin";
256
+ function escapeOsaScript2(script) {
257
+ return script.replace(/'/g, `'"'"'`);
258
+ }
259
+ function osascript2(script) {
260
+ return execSync2(`osascript -e '${escapeOsaScript2(script)}'`, {
261
+ encoding: "utf8",
262
+ timeout: 7000,
263
+ stdio: ["ignore", "pipe", "pipe"]
264
+ }).trim();
265
+ }
64
266
  function cookieScript() {
65
267
  return `
66
268
  tell application "Google Chrome"
@@ -75,17 +277,17 @@ function cookieScript() {
75
277
  end tell
76
278
  `;
77
279
  }
78
- var TEAM_JSON_PATHS = [
280
+ var TEAM_JSON_PATHS2 = [
79
281
  "JSON.stringify(JSON.parse(localStorage.localConfig_v2).teams)",
80
282
  "JSON.stringify(JSON.parse(localStorage.localConfig_v3).teams)",
81
283
  "JSON.stringify(JSON.parse(localStorage.getItem('reduxPersist:localConfig'))?.teams || {})",
82
284
  "JSON.stringify(window.boot_data?.teams || {})"
83
285
  ];
84
- function isRecord(value) {
286
+ function isRecord2(value) {
85
287
  return typeof value === "object" && value !== null;
86
288
  }
87
289
  function toChromeTeam(value) {
88
- if (!isRecord(value)) {
290
+ if (!isRecord2(value)) {
89
291
  return null;
90
292
  }
91
293
  const token = typeof value.token === "string" ? value.token : null;
@@ -96,8 +298,8 @@ function toChromeTeam(value) {
96
298
  const name = typeof value.name === "string" ? value.name : undefined;
97
299
  return { url, name, token };
98
300
  }
99
- function teamsScript() {
100
- const tryPaths = TEAM_JSON_PATHS.map((expr) => `try { var v = ${expr}; if (v && v !== '{}' && v !== 'null') return v; } catch(e) {}`);
301
+ function teamsScript2() {
302
+ const tryPaths = TEAM_JSON_PATHS2.map((expr) => `try { var v = ${expr}; if (v && v !== '{}' && v !== 'null') return v; } catch(e) {}`);
101
303
  return `
102
304
  tell application "Google Chrome"
103
305
  repeat with w in windows
@@ -112,22 +314,22 @@ function teamsScript() {
112
314
  `;
113
315
  }
114
316
  function extractFromChrome() {
115
- if (!IS_MACOS) {
317
+ if (!IS_MACOS2) {
116
318
  return null;
117
319
  }
118
320
  try {
119
- const cookie = osascript(cookieScript());
321
+ const cookie = osascript2(cookieScript());
120
322
  if (!cookie || !cookie.startsWith("xoxd-")) {
121
323
  return null;
122
324
  }
123
- const teamsRaw = osascript(teamsScript());
325
+ const teamsRaw = osascript2(teamsScript2());
124
326
  let teamsObj = {};
125
327
  try {
126
328
  teamsObj = JSON.parse(teamsRaw || "{}");
127
329
  } catch {
128
330
  teamsObj = {};
129
331
  }
130
- const teamsRecord = isRecord(teamsObj) ? teamsObj : {};
332
+ const teamsRecord = isRecord2(teamsObj) ? teamsObj : {};
131
333
  const teams = Object.values(teamsRecord).map((t) => toChromeTeam(t)).filter((t) => t !== null);
132
334
  if (teams.length === 0) {
133
335
  return null;
@@ -175,15 +377,15 @@ function parseSlackCurlCommand(curlInput) {
175
377
 
176
378
  // src/auth/desktop.ts
177
379
  import { cp, mkdir, rm, unlink } from "node:fs/promises";
178
- import { existsSync as existsSync2 } from "node:fs";
179
- import { execFileSync } from "node:child_process";
180
- import { pbkdf2Sync, createDecipheriv } from "node:crypto";
181
- import { homedir, platform as platform2 } from "node:os";
182
- import { join as join3 } from "node:path";
380
+ import { existsSync as existsSync3 } from "node:fs";
381
+ import { execFileSync as execFileSync2 } from "node:child_process";
382
+ import { pbkdf2Sync as pbkdf2Sync2, createDecipheriv as createDecipheriv2 } from "node:crypto";
383
+ import { homedir as homedir2, platform as platform3 } from "node:os";
384
+ import { join as join4 } from "node:path";
183
385
 
184
386
  // src/lib/leveldb-reader.ts
185
387
  import { readdir, readFile } from "node:fs/promises";
186
- import { join as join2 } from "node:path";
388
+ import { join as join3 } from "node:path";
187
389
  import { snappyUncompress } from "hysnappy";
188
390
  var LEVELDB_MAGIC = Buffer.from([87, 251, 128, 139, 36, 117, 71, 219]);
189
391
  var COMPRESSION_NONE = 0;
@@ -425,12 +627,12 @@ async function readChromiumLevelDB(dir) {
425
627
  }
426
628
  const sstFiles = files.filter((f) => f.endsWith(".ldb") || f.endsWith(".sst"));
427
629
  for (const file of sstFiles) {
428
- const fileEntries = await parseSSTable(join2(dir, file));
630
+ const fileEntries = await parseSSTable(join3(dir, file));
429
631
  entries.push(...fileEntries);
430
632
  }
431
633
  const logFiles = files.filter((f) => f.endsWith(".log"));
432
634
  for (const file of logFiles) {
433
- const fileEntries = await parseLogFile(join2(dir, file));
635
+ const fileEntries = await parseLogFile(join3(dir, file));
434
636
  entries.push(...fileEntries);
435
637
  }
436
638
  return entries;
@@ -441,7 +643,7 @@ async function findKeysContaining(dir, substring) {
441
643
  }
442
644
 
443
645
  // src/auth/desktop.ts
444
- function isMissingBunSqliteModule(error) {
646
+ function isMissingBunSqliteModule2(error) {
445
647
  if (!error || typeof error !== "object") {
446
648
  return false;
447
649
  }
@@ -456,7 +658,7 @@ function isMissingBunSqliteModule(error) {
456
658
  }
457
659
  return message.includes("Cannot find module") || message.includes("Unknown builtin module") || message.includes("unsupported URL scheme") || message.includes("Only URLs with a scheme in");
458
660
  }
459
- async function queryReadonlySqlite(dbPath, sql) {
661
+ async function queryReadonlySqlite2(dbPath, sql) {
460
662
  try {
461
663
  const { Database } = await import("bun:sqlite");
462
664
  const db = new Database(dbPath, { readonly: true });
@@ -466,7 +668,7 @@ async function queryReadonlySqlite(dbPath, sql) {
466
668
  db.close();
467
669
  }
468
670
  } catch (error) {
469
- if (!isMissingBunSqliteModule(error)) {
671
+ if (!isMissingBunSqliteModule2(error)) {
470
672
  throw error;
471
673
  }
472
674
  const { DatabaseSync } = await import("node:sqlite");
@@ -478,35 +680,35 @@ async function queryReadonlySqlite(dbPath, sql) {
478
680
  }
479
681
  }
480
682
  }
481
- var PLATFORM = platform2();
482
- var IS_MACOS2 = PLATFORM === "darwin";
683
+ var PLATFORM = platform3();
684
+ var IS_MACOS3 = PLATFORM === "darwin";
483
685
  var IS_LINUX = PLATFORM === "linux";
484
- var SLACK_SUPPORT_DIR_ELECTRON = join3(homedir(), "Library", "Application Support", "Slack");
485
- var SLACK_SUPPORT_DIR_APPSTORE = join3(homedir(), "Library", "Containers", "com.tinyspeck.slackmacgap", "Data", "Library", "Application Support", "Slack");
486
- var SLACK_SUPPORT_DIR_LINUX = join3(homedir(), ".config", "Slack");
487
- var SLACK_SUPPORT_DIR_LINUX_FLATPAK = join3(homedir(), ".var", "app", "com.slack.Slack", "config", "Slack");
686
+ var SLACK_SUPPORT_DIR_ELECTRON = join4(homedir2(), "Library", "Application Support", "Slack");
687
+ var SLACK_SUPPORT_DIR_APPSTORE = join4(homedir2(), "Library", "Containers", "com.tinyspeck.slackmacgap", "Data", "Library", "Application Support", "Slack");
688
+ var SLACK_SUPPORT_DIR_LINUX = join4(homedir2(), ".config", "Slack");
689
+ var SLACK_SUPPORT_DIR_LINUX_FLATPAK = join4(homedir2(), ".var", "app", "com.slack.Slack", "config", "Slack");
488
690
  function getSlackPaths() {
489
- const candidates = IS_MACOS2 ? [SLACK_SUPPORT_DIR_ELECTRON, SLACK_SUPPORT_DIR_APPSTORE] : IS_LINUX ? [SLACK_SUPPORT_DIR_LINUX_FLATPAK, SLACK_SUPPORT_DIR_LINUX] : [];
691
+ const candidates = IS_MACOS3 ? [SLACK_SUPPORT_DIR_ELECTRON, SLACK_SUPPORT_DIR_APPSTORE] : IS_LINUX ? [SLACK_SUPPORT_DIR_LINUX_FLATPAK, SLACK_SUPPORT_DIR_LINUX] : [];
490
692
  if (candidates.length === 0) {
491
693
  throw new Error(`Slack Desktop extraction is not supported on ${PLATFORM}.`);
492
694
  }
493
695
  for (const dir of candidates) {
494
- const leveldbDir = join3(dir, "Local Storage", "leveldb");
495
- if (existsSync2(leveldbDir)) {
496
- const cookiesDbCandidates = [join3(dir, "Network", "Cookies"), join3(dir, "Cookies")];
497
- const cookiesDb = cookiesDbCandidates.find((candidate) => existsSync2(candidate)) || cookiesDbCandidates[0];
696
+ const leveldbDir = join4(dir, "Local Storage", "leveldb");
697
+ if (existsSync3(leveldbDir)) {
698
+ const cookiesDbCandidates = [join4(dir, "Network", "Cookies"), join4(dir, "Cookies")];
699
+ const cookiesDb = cookiesDbCandidates.find((candidate) => existsSync3(candidate)) || cookiesDbCandidates[0];
498
700
  return { leveldbDir, cookiesDb };
499
701
  }
500
702
  }
501
703
  throw new Error(`Slack Desktop data not found. Checked:
502
- - ${candidates.map((d) => join3(d, "Local Storage", "leveldb")).join(`
704
+ - ${candidates.map((d) => join4(d, "Local Storage", "leveldb")).join(`
503
705
  - `)}`);
504
706
  }
505
- function isRecord2(value) {
707
+ function isRecord3(value) {
506
708
  return typeof value === "object" && value !== null;
507
709
  }
508
710
  function toDesktopTeam(value) {
509
- if (!isRecord2(value)) {
711
+ if (!isRecord3(value)) {
510
712
  return null;
511
713
  }
512
714
  const url = typeof value.url === "string" ? value.url : null;
@@ -518,13 +720,13 @@ function toDesktopTeam(value) {
518
720
  return { url, name, token };
519
721
  }
520
722
  async function snapshotLevelDb(srcDir) {
521
- const base = join3(homedir(), ".config", "agent-slack", "cache", "leveldb-snapshots");
522
- const dest = join3(base, `${Date.now()}`);
723
+ const base = join4(homedir2(), ".config", "agent-slack", "cache", "leveldb-snapshots");
724
+ const dest = join4(base, `${Date.now()}`);
523
725
  await mkdir(base, { recursive: true });
524
- let useNodeCopy = !IS_MACOS2;
525
- if (IS_MACOS2) {
726
+ let useNodeCopy = !IS_MACOS3;
727
+ if (IS_MACOS3) {
526
728
  try {
527
- execFileSync("cp", ["-cR", srcDir, dest], {
729
+ execFileSync2("cp", ["-cR", srcDir, dest], {
528
730
  stdio: ["ignore", "ignore", "ignore"]
529
731
  });
530
732
  } catch {
@@ -535,7 +737,7 @@ async function snapshotLevelDb(srcDir) {
535
737
  await cp(srcDir, dest, { recursive: true, force: true });
536
738
  }
537
739
  try {
538
- await unlink(join3(dest, "LOCK"));
740
+ await unlink(join4(dest, "LOCK"));
539
741
  } catch {}
540
742
  return dest;
541
743
  }
@@ -577,7 +779,7 @@ function parseLocalConfig(raw) {
577
779
  throw lastErr || new Error("localConfig not parseable");
578
780
  }
579
781
  async function extractTeamsFromSlackLevelDb(leveldbDir) {
580
- if (!existsSync2(leveldbDir)) {
782
+ if (!existsSync3(leveldbDir)) {
581
783
  throw new Error(`Slack LevelDB not found: ${leveldbDir}`);
582
784
  }
583
785
  const snap = await snapshotLevelDb(leveldbDir);
@@ -605,8 +807,8 @@ async function extractTeamsFromSlackLevelDb(leveldbDir) {
605
807
  throw new Error("Slack LevelDB did not contain localConfig_v2/v3");
606
808
  }
607
809
  const cfg = parseLocalConfig(configBuf);
608
- const teamsValue = isRecord2(cfg) ? cfg.teams : undefined;
609
- const teamsObj = isRecord2(teamsValue) ? teamsValue : {};
810
+ const teamsValue = isRecord3(cfg) ? cfg.teams : undefined;
811
+ const teamsObj = isRecord3(teamsValue) ? teamsValue : {};
610
812
  const teams = Object.values(teamsObj).map((t) => toDesktopTeam(t)).filter((t) => t !== null).filter((t) => t.token.startsWith("xoxc-"));
611
813
  if (teams.length === 0) {
612
814
  throw new Error("No xoxc tokens found in Slack localConfig");
@@ -618,13 +820,13 @@ async function extractTeamsFromSlackLevelDb(leveldbDir) {
618
820
  } catch {}
619
821
  }
620
822
  }
621
- function getSafeStoragePasswords(prefix) {
622
- if (IS_MACOS2) {
823
+ function getSafeStoragePasswords2(prefix) {
824
+ if (IS_MACOS3) {
623
825
  const services = ["Slack Safe Storage", "Chrome Safe Storage", "Chromium Safe Storage"];
624
826
  const passwords = [];
625
827
  for (const service of services) {
626
828
  try {
627
- const out = execFileSync("security", ["find-generic-password", "-w", "-s", service], {
829
+ const out = execFileSync2("security", ["find-generic-password", "-w", "-s", service], {
628
830
  encoding: "utf8",
629
831
  stdio: ["ignore", "pipe", "ignore"]
630
832
  }).trim();
@@ -647,7 +849,7 @@ function getSafeStoragePasswords(prefix) {
647
849
  const passwords = [];
648
850
  for (const pair of attributes) {
649
851
  try {
650
- const out = execFileSync("secret-tool", ["lookup", ...pair], {
852
+ const out = execFileSync2("secret-tool", ["lookup", ...pair], {
651
853
  encoding: "utf8",
652
854
  stdio: ["ignore", "pipe", "ignore"]
653
855
  }).trim();
@@ -664,14 +866,14 @@ function getSafeStoragePasswords(prefix) {
664
866
  }
665
867
  throw new Error("Could not read Safe Storage password from desktop keychain.");
666
868
  }
667
- function decryptChromiumCookieValue(data, password) {
869
+ function decryptChromiumCookieValue2(data, password) {
668
870
  if (!data || data.length === 0) {
669
871
  return "";
670
872
  }
671
873
  const salt = Buffer.from("saltysalt", "utf8");
672
874
  const iv = Buffer.alloc(16, " ");
673
- const key = pbkdf2Sync(password, salt, IS_LINUX ? 1 : 1003, 16, "sha1");
674
- const decipher = createDecipheriv("aes-128-cbc", key, iv);
875
+ const key = pbkdf2Sync2(password, salt, IS_LINUX ? 1 : 1003, 16, "sha1");
876
+ const decipher = createDecipheriv2("aes-128-cbc", key, iv);
675
877
  decipher.setAutoPadding(true);
676
878
  const plain = Buffer.concat([decipher.update(data), decipher.final()]);
677
879
  const marker = Buffer.from("xoxd-");
@@ -695,10 +897,10 @@ function decryptChromiumCookieValue(data, password) {
695
897
  }
696
898
  }
697
899
  async function extractCookieDFromSlackCookiesDb(cookiesPath) {
698
- if (!existsSync2(cookiesPath)) {
900
+ if (!existsSync3(cookiesPath)) {
699
901
  throw new Error(`Slack Cookies DB not found: ${cookiesPath}`);
700
902
  }
701
- const rows = await queryReadonlySqlite(cookiesPath, "select host_key, name, value, encrypted_value from cookies where name = 'd' and host_key like '%slack.com' order by length(encrypted_value) desc");
903
+ const rows = await queryReadonlySqlite2(cookiesPath, "select host_key, name, value, encrypted_value from cookies where name = 'd' and host_key like '%slack.com' order by length(encrypted_value) desc");
702
904
  if (!rows || rows.length === 0) {
703
905
  throw new Error("No Slack 'd' cookie found");
704
906
  }
@@ -712,10 +914,10 @@ async function extractCookieDFromSlackCookiesDb(cookiesPath) {
712
914
  }
713
915
  const prefix = encrypted.subarray(0, 3).toString("utf8");
714
916
  const data = prefix === "v10" || prefix === "v11" ? encrypted.subarray(3) : encrypted;
715
- const passwords = getSafeStoragePasswords(prefix);
917
+ const passwords = getSafeStoragePasswords2(prefix);
716
918
  for (const password of passwords) {
717
919
  try {
718
- const decrypted = decryptChromiumCookieValue(data, password);
920
+ const decrypted = decryptChromiumCookieValue2(data, password);
719
921
  const match = decrypted.match(/xoxd-[A-Za-z0-9%/+_=.-]+/);
720
922
  if (match) {
721
923
  return match[0];
@@ -736,10 +938,10 @@ async function extractFromSlackDesktop() {
736
938
  }
737
939
 
738
940
  // src/auth/paths.ts
739
- import { homedir as homedir2 } from "node:os";
740
- import { join as join4 } from "node:path";
741
- var AGENT_SLACK_DIR = join4(homedir2(), ".config", "agent-slack");
742
- var CREDENTIALS_FILE = join4(AGENT_SLACK_DIR, "credentials.json");
941
+ import { homedir as homedir3 } from "node:os";
942
+ import { join as join5 } from "node:path";
943
+ var AGENT_SLACK_DIR = join5(homedir3(), ".config", "agent-slack");
944
+ var CREDENTIALS_FILE = join5(AGENT_SLACK_DIR, "credentials.json");
743
945
  var KEYCHAIN_SERVICE = "agent-slack";
744
946
 
745
947
  // src/lib/fs.ts
@@ -750,7 +952,7 @@ async function readJsonFile(path) {
750
952
  const raw = await readFile2(path, "utf8");
751
953
  return JSON.parse(raw);
752
954
  } catch (err) {
753
- if (isRecord3(err) && err.code === "ENOENT") {
955
+ if (isRecord4(err) && err.code === "ENOENT") {
754
956
  return null;
755
957
  }
756
958
  if (err instanceof SyntaxError) {
@@ -764,7 +966,7 @@ async function writeJsonFile(path, data) {
764
966
  await writeFile(path, `${JSON.stringify(data, null, 2)}
765
967
  `, { mode: 384 });
766
968
  }
767
- function isRecord3(value) {
969
+ function isRecord4(value) {
768
970
  return typeof value === "object" && value !== null;
769
971
  }
770
972
 
@@ -796,32 +998,32 @@ var CredentialsSchema = z.object({
796
998
  });
797
999
 
798
1000
  // src/auth/keychain.ts
799
- import { platform as platform3 } from "node:os";
800
- import { execFileSync as execFileSync2 } from "node:child_process";
801
- var IS_MACOS3 = platform3() === "darwin";
1001
+ import { platform as platform4 } from "node:os";
1002
+ import { execFileSync as execFileSync3 } from "node:child_process";
1003
+ var IS_MACOS4 = platform4() === "darwin";
802
1004
  function keychainGet(account, service) {
803
- if (!IS_MACOS3) {
1005
+ if (!IS_MACOS4) {
804
1006
  return null;
805
1007
  }
806
1008
  try {
807
- const result = execFileSync2("security", ["find-generic-password", "-s", service, "-a", account, "-w"], { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] });
1009
+ const result = execFileSync3("security", ["find-generic-password", "-s", service, "-a", account, "-w"], { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] });
808
1010
  return result.trim() || null;
809
1011
  } catch {
810
1012
  return null;
811
1013
  }
812
1014
  }
813
1015
  function keychainSet(input) {
814
- if (!IS_MACOS3) {
1016
+ if (!IS_MACOS4) {
815
1017
  return false;
816
1018
  }
817
1019
  const { account, value, service } = input;
818
1020
  try {
819
1021
  try {
820
- execFileSync2("security", ["delete-generic-password", "-s", service, "-a", account], {
1022
+ execFileSync3("security", ["delete-generic-password", "-s", service, "-a", account], {
821
1023
  stdio: ["pipe", "pipe", "ignore"]
822
1024
  });
823
1025
  } catch {}
824
- execFileSync2("security", ["add-generic-password", "-s", service, "-a", account, "-w", value], {
1026
+ execFileSync3("security", ["add-generic-password", "-s", service, "-a", account, "-w", value], {
825
1027
  stdio: "pipe"
826
1028
  });
827
1029
  return true;
@@ -831,9 +1033,9 @@ function keychainSet(input) {
831
1033
  }
832
1034
 
833
1035
  // src/auth/store.ts
834
- import { platform as platform4 } from "node:os";
1036
+ import { platform as platform5 } from "node:os";
835
1037
  var KEYCHAIN_PLACEHOLDER = "__KEYCHAIN__";
836
- var IS_MACOS4 = platform4() === "darwin";
1038
+ var IS_MACOS5 = platform5() === "darwin";
837
1039
  function normalizeWorkspaceUrl(workspaceUrl) {
838
1040
  const u = new URL(workspaceUrl);
839
1041
  return `${u.protocol}//${u.host}`;
@@ -887,7 +1089,7 @@ async function saveCredentials(credentials) {
887
1089
  }))
888
1090
  };
889
1091
  const filePayload = structuredClone(payload);
890
- if (IS_MACOS4) {
1092
+ if (IS_MACOS5) {
891
1093
  const firstBrowser = payload.workspaces.find((w) => w.auth.auth_type === "browser");
892
1094
  let xoxdStored = false;
893
1095
  if (firstBrowser?.auth.auth_type === "browser" && !isPlaceholderSecret(firstBrowser.auth.xoxd_cookie)) {
@@ -994,6 +1196,64 @@ async function resolveDefaultWorkspace() {
994
1196
  return creds.workspaces[0] ?? null;
995
1197
  }
996
1198
 
1199
+ // src/cli/workspace-selector.ts
1200
+ function normalizeUrl(u) {
1201
+ const url = new URL(u);
1202
+ return `${url.protocol}//${url.host}`;
1203
+ }
1204
+ function normalizedWorkspaceCandidates(workspace) {
1205
+ let host = "";
1206
+ try {
1207
+ host = new URL(workspace.workspace_url).host.toLowerCase();
1208
+ } catch {
1209
+ host = "";
1210
+ }
1211
+ const hostWithoutSlackSuffix = host.replace(/\.slack\.com$/i, "");
1212
+ return [
1213
+ workspace.workspace_url.toLowerCase(),
1214
+ host,
1215
+ hostWithoutSlackSuffix,
1216
+ workspace.workspace_name?.toLowerCase() ?? "",
1217
+ workspace.team_domain?.toLowerCase() ?? ""
1218
+ ].filter(Boolean);
1219
+ }
1220
+ function resolveWorkspaceSelector(workspaces, selector) {
1221
+ const raw = selector.trim();
1222
+ if (!raw) {
1223
+ return { match: null, ambiguous: [] };
1224
+ }
1225
+ try {
1226
+ const normalized = normalizeUrl(raw).toLowerCase();
1227
+ const exact = workspaces.find((w) => w.workspace_url.toLowerCase() === normalized);
1228
+ if (exact) {
1229
+ return { match: exact, ambiguous: [] };
1230
+ }
1231
+ } catch {}
1232
+ const needle = raw.toLowerCase();
1233
+ const matches = workspaces.filter((workspace) => normalizedWorkspaceCandidates(workspace).some((candidate) => candidate.includes(needle)));
1234
+ if (matches.length === 1) {
1235
+ return { match: matches[0], ambiguous: [] };
1236
+ }
1237
+ if (matches.length > 1) {
1238
+ return { match: null, ambiguous: matches };
1239
+ }
1240
+ return { match: null, ambiguous: [] };
1241
+ }
1242
+
1243
+ // src/lib/object-type-guards.ts
1244
+ function isRecord5(value) {
1245
+ return typeof value === "object" && value !== null;
1246
+ }
1247
+ function asArray(value) {
1248
+ return Array.isArray(value) ? value : [];
1249
+ }
1250
+ function getString(value) {
1251
+ return typeof value === "string" ? value : undefined;
1252
+ }
1253
+ function getNumber(value) {
1254
+ return typeof value === "number" ? value : undefined;
1255
+ }
1256
+
997
1257
  // src/slack/channels.ts
998
1258
  function isChannelId(input) {
999
1259
  return /^[CDG][A-Z0-9]{8,}$/.test(input);
@@ -1017,8 +1277,24 @@ async function resolveChannelId(client, input) {
1017
1277
  if (!name) {
1018
1278
  throw new Error("Channel name is empty");
1019
1279
  }
1280
+ try {
1281
+ const searchResp = await client.api("search.messages", {
1282
+ query: `in:#${name}`,
1283
+ count: 1,
1284
+ sort: "timestamp",
1285
+ sort_dir: "desc"
1286
+ });
1287
+ const messages = isRecord5(searchResp) ? searchResp.messages : null;
1288
+ const matches = isRecord5(messages) ? asArray(messages.matches).filter(isRecord5) : [];
1289
+ if (matches.length > 0) {
1290
+ const channel = isRecord5(matches[0].channel) ? matches[0].channel : null;
1291
+ const channelId = channel ? getString(channel.id) : undefined;
1292
+ if (channelId) {
1293
+ return channelId;
1294
+ }
1295
+ }
1296
+ } catch {}
1020
1297
  let cursor;
1021
- const matches = [];
1022
1298
  for (;; ) {
1023
1299
  const resp = await client.api("conversations.list", {
1024
1300
  exclude_archived: true,
@@ -1026,39 +1302,20 @@ async function resolveChannelId(client, input) {
1026
1302
  cursor,
1027
1303
  types: "public_channel,private_channel"
1028
1304
  });
1029
- const chans = asArray(resp.channels).filter(isRecord4);
1305
+ const chans = asArray(resp.channels).filter(isRecord5);
1030
1306
  for (const c of chans) {
1031
1307
  if (getString(c.name) === name && getString(c.id)) {
1032
- matches.push({
1033
- id: getString(c.id) ?? "",
1034
- name: getString(c.name) ?? undefined,
1035
- is_private: typeof c.is_private === "boolean" ? c.is_private : undefined
1036
- });
1308
+ return getString(c.id);
1037
1309
  }
1038
1310
  }
1039
- const meta = isRecord4(resp.response_metadata) ? resp.response_metadata : null;
1311
+ const meta = isRecord5(resp.response_metadata) ? resp.response_metadata : null;
1040
1312
  const next = meta ? getString(meta.next_cursor) : undefined;
1041
1313
  if (!next) {
1042
1314
  break;
1043
1315
  }
1044
1316
  cursor = next;
1045
1317
  }
1046
- if (matches.length === 1) {
1047
- return matches[0].id;
1048
- }
1049
- if (matches.length === 0) {
1050
- throw new Error(`Could not resolve channel name: #${name}`);
1051
- }
1052
- throw new Error(`Ambiguous channel name: #${name} (matched ${matches.length} channels: ${matches.map((m) => m.id).join(", ")})`);
1053
- }
1054
- function isRecord4(value) {
1055
- return typeof value === "object" && value !== null;
1056
- }
1057
- function asArray(value) {
1058
- return Array.isArray(value) ? value : [];
1059
- }
1060
- function getString(value) {
1061
- return typeof value === "string" ? value : undefined;
1318
+ throw new Error(`Could not resolve channel name: #${name}`);
1062
1319
  }
1063
1320
 
1064
1321
  // src/slack/client.ts
@@ -1126,14 +1383,14 @@ class SlackApiClient {
1126
1383
  if (!response.ok) {
1127
1384
  throw new Error(`Slack HTTP ${response.status} calling ${input.method}`);
1128
1385
  }
1129
- if (!isRecord5(data) || data.ok !== true) {
1130
- const error = isRecord5(data) && typeof data.error === "string" ? data.error : null;
1386
+ if (!isRecord6(data) || data.ok !== true) {
1387
+ const error = isRecord6(data) && typeof data.error === "string" ? data.error : null;
1131
1388
  throw new Error(error || `Slack API error calling ${input.method}`);
1132
1389
  }
1133
1390
  return data;
1134
1391
  }
1135
1392
  }
1136
- function isRecord5(value) {
1393
+ function isRecord6(value) {
1137
1394
  return typeof value === "object" && value !== null;
1138
1395
  }
1139
1396
 
@@ -1164,16 +1421,23 @@ async function assertWorkspaceSpecifiedForChannelNames(input) {
1164
1421
  return;
1165
1422
  }
1166
1423
  if (!input.workspaceUrl) {
1167
- throw new Error('Ambiguous channel name across multiple workspaces. Pass --workspace "https://...slack.com" (or set SLACK_WORKSPACE_URL).');
1424
+ throw new Error('Ambiguous channel name across multiple workspaces. Pass --workspace "<url-or-unique-substring>" (or set SLACK_WORKSPACE_URL).');
1168
1425
  }
1169
1426
  }
1170
1427
  function isAuthErrorMessage(message) {
1171
1428
  return /(?:^|[^a-z])(invalid_auth|token_expired)(?:$|[^a-z])/i.test(message);
1172
1429
  }
1173
- function normalizeUrl(u) {
1430
+ function normalizeUrl2(u) {
1174
1431
  const url = new URL(u);
1175
1432
  return `${url.protocol}//${url.host}`;
1176
1433
  }
1434
+ function tryNormalizeUrl(u) {
1435
+ try {
1436
+ return normalizeUrl2(u);
1437
+ } catch {
1438
+ return;
1439
+ }
1440
+ }
1177
1441
  async function refreshFromDesktopIfPossible() {
1178
1442
  if (process.platform !== "darwin" && process.platform !== "linux") {
1179
1443
  return false;
@@ -1181,7 +1445,7 @@ async function refreshFromDesktopIfPossible() {
1181
1445
  try {
1182
1446
  const extracted = await extractFromSlackDesktop();
1183
1447
  await upsertWorkspaces(extracted.teams.map((team) => ({
1184
- workspace_url: normalizeUrl(team.url),
1448
+ workspace_url: normalizeUrl2(team.url),
1185
1449
  workspace_name: team.name,
1186
1450
  auth: {
1187
1451
  auth_type: "browser",
@@ -1227,18 +1491,35 @@ function pickAuthFromEnv() {
1227
1491
  return { auth_type: "standard", token };
1228
1492
  }
1229
1493
  async function getClientForWorkspace(workspaceUrl) {
1494
+ const selector = workspaceUrl?.trim() || undefined;
1495
+ const normalizedSelectorUrl = selector ? tryNormalizeUrl(selector) : undefined;
1496
+ const selectorProvided = Boolean(selector);
1497
+ let resolvedWorkspaceUrl = normalizedSelectorUrl;
1498
+ if (selector) {
1499
+ const creds = await loadCredentials();
1500
+ const resolved = resolveWorkspaceSelector(creds.workspaces, selector);
1501
+ if (resolved.ambiguous.length > 0) {
1502
+ const options = resolved.ambiguous.map((w) => w.workspace_url).join(", ");
1503
+ throw new Error(`Workspace selector "${selector}" is ambiguous. Matches: ${options}. Pass a more specific selector or full workspace URL.`);
1504
+ }
1505
+ if (resolved.match) {
1506
+ resolvedWorkspaceUrl = resolved.match.workspace_url;
1507
+ } else if (!normalizedSelectorUrl) {
1508
+ resolvedWorkspaceUrl = undefined;
1509
+ }
1510
+ }
1230
1511
  const env = pickAuthFromEnv();
1231
1512
  if (env) {
1232
1513
  const envWorkspaceUrl = process.env.SLACK_WORKSPACE_URL?.trim();
1233
- const urlForBrowser = workspaceUrl || envWorkspaceUrl;
1514
+ const urlForBrowser = resolvedWorkspaceUrl || envWorkspaceUrl;
1234
1515
  return {
1235
1516
  client: new SlackApiClient(env, { workspaceUrl: urlForBrowser }),
1236
1517
  auth: env,
1237
1518
  workspace_url: urlForBrowser
1238
1519
  };
1239
1520
  }
1240
- if (workspaceUrl) {
1241
- const ws = await resolveWorkspaceForUrl(workspaceUrl);
1521
+ if (resolvedWorkspaceUrl) {
1522
+ const ws = await resolveWorkspaceForUrl(resolvedWorkspaceUrl);
1242
1523
  if (ws) {
1243
1524
  return {
1244
1525
  client: new SlackApiClient(ws.auth, {
@@ -1249,20 +1530,22 @@ async function getClientForWorkspace(workspaceUrl) {
1249
1530
  };
1250
1531
  }
1251
1532
  }
1252
- const def = await resolveDefaultWorkspace();
1253
- if (def) {
1254
- return {
1255
- client: new SlackApiClient(def.auth, {
1256
- workspaceUrl: def.workspace_url
1257
- }),
1258
- auth: def.auth,
1259
- workspace_url: def.workspace_url
1260
- };
1533
+ if (!selectorProvided) {
1534
+ const def = await resolveDefaultWorkspace();
1535
+ if (def) {
1536
+ return {
1537
+ client: new SlackApiClient(def.auth, {
1538
+ workspaceUrl: def.workspace_url
1539
+ }),
1540
+ auth: def.auth,
1541
+ workspace_url: def.workspace_url
1542
+ };
1543
+ }
1261
1544
  }
1262
1545
  try {
1263
1546
  const extracted = await extractFromSlackDesktop();
1264
1547
  await upsertWorkspaces(extracted.teams.map((team) => ({
1265
- workspace_url: normalizeUrl(team.url),
1548
+ workspace_url: normalizeUrl2(team.url),
1266
1549
  workspace_name: team.name,
1267
1550
  auth: {
1268
1551
  auth_type: "browser",
@@ -1270,8 +1553,8 @@ async function getClientForWorkspace(workspaceUrl) {
1270
1553
  xoxd_cookie: extracted.cookie_d
1271
1554
  }
1272
1555
  })));
1273
- const desired = workspaceUrl ? await resolveWorkspaceForUrl(workspaceUrl) : await resolveDefaultWorkspace();
1274
- const chosen = desired ?? await resolveDefaultWorkspace();
1556
+ const desired = resolvedWorkspaceUrl ? await resolveWorkspaceForUrl(resolvedWorkspaceUrl) : selector ? resolveWorkspaceSelector((await loadCredentials()).workspaces, selector).match : await resolveDefaultWorkspace();
1557
+ const chosen = desired ?? (!selectorProvided ? await resolveDefaultWorkspace() : null);
1275
1558
  if (chosen) {
1276
1559
  return {
1277
1560
  client: new SlackApiClient(chosen.auth, {
@@ -1282,16 +1565,38 @@ async function getClientForWorkspace(workspaceUrl) {
1282
1565
  };
1283
1566
  }
1284
1567
  } catch {}
1285
- const chrome = extractFromChrome();
1568
+ const chrome = extractFromChrome() ?? await extractFromBrave();
1286
1569
  if (chrome && chrome.teams.length > 0) {
1287
- const chosen = (workspaceUrl ? chrome.teams.find((t) => normalizeUrl(t.url) === normalizeUrl(workspaceUrl)) : null) ?? chrome.teams[0];
1570
+ let chosen = chrome.teams[0];
1571
+ if (selector) {
1572
+ const normalizedSelector = selector.toLowerCase();
1573
+ const matches = chrome.teams.filter((t) => {
1574
+ const normalizedUrl = normalizeUrl2(t.url).toLowerCase();
1575
+ const host = new URL(t.url).host.toLowerCase();
1576
+ const hostWithoutSlackSuffix = host.replace(/\.slack\.com$/i, "");
1577
+ const name = t.name?.toLowerCase() ?? "";
1578
+ return normalizedUrl.includes(normalizedSelector) || host.includes(normalizedSelector) || hostWithoutSlackSuffix.includes(normalizedSelector) || name.includes(normalizedSelector);
1579
+ });
1580
+ if (matches.length > 1) {
1581
+ throw new Error(`Workspace selector "${selector}" is ambiguous in Chrome workspaces. Matches: ${matches.map((t) => normalizeUrl2(t.url)).join(", ")}. Pass a more specific selector or full workspace URL.`);
1582
+ }
1583
+ if (matches.length === 1) {
1584
+ chosen = matches[0];
1585
+ } else if (normalizedSelectorUrl) {
1586
+ try {
1587
+ chosen = chrome.teams.find((t) => normalizeUrl2(t.url) === normalizeUrl2(selector)) ?? chosen;
1588
+ } catch {}
1589
+ } else {
1590
+ throw new Error(`No configured workspace matches selector "${selector}". Run "agent-slack auth whoami" to list available workspaces.`);
1591
+ }
1592
+ }
1288
1593
  const auth = {
1289
1594
  auth_type: "browser",
1290
1595
  xoxc_token: chosen.token,
1291
1596
  xoxd_cookie: chrome.cookie_d
1292
1597
  };
1293
1598
  await upsertWorkspace({
1294
- workspace_url: normalizeUrl(chosen.url),
1599
+ workspace_url: normalizeUrl2(chosen.url),
1295
1600
  workspace_name: chosen.name,
1296
1601
  auth: {
1297
1602
  auth_type: "browser",
@@ -1301,13 +1606,16 @@ async function getClientForWorkspace(workspaceUrl) {
1301
1606
  });
1302
1607
  return {
1303
1608
  client: new SlackApiClient(auth, {
1304
- workspaceUrl: normalizeUrl(chosen.url)
1609
+ workspaceUrl: normalizeUrl2(chosen.url)
1305
1610
  }),
1306
1611
  auth,
1307
- workspace_url: normalizeUrl(chosen.url)
1612
+ workspace_url: normalizeUrl2(chosen.url)
1308
1613
  };
1309
1614
  }
1310
- throw new Error('No Slack credentials available. Try "agent-slack auth import-desktop" or set SLACK_TOKEN / SLACK_COOKIE_D.');
1615
+ if (selector && !normalizedSelectorUrl) {
1616
+ throw new Error(`No configured workspace matches selector "${selector}". Run "agent-slack auth whoami" to list available workspaces.`);
1617
+ }
1618
+ throw new Error('No Slack credentials available. Try "agent-slack auth import-desktop", "agent-slack auth import-chrome", "agent-slack auth import-brave", or set SLACK_TOKEN / SLACK_COOKIE_D.');
1311
1619
  }
1312
1620
  function createCliContext() {
1313
1621
  return {
@@ -1315,12 +1623,13 @@ function createCliContext() {
1315
1623
  assertWorkspaceSpecifiedForChannelNames,
1316
1624
  withAutoRefresh,
1317
1625
  getClientForWorkspace,
1318
- normalizeUrl,
1626
+ normalizeUrl: normalizeUrl2,
1319
1627
  errorMessage,
1320
1628
  parseContentType,
1321
1629
  parseCurl: parseSlackCurlCommand,
1322
1630
  importDesktop: extractFromSlackDesktop,
1323
- importChrome: extractFromChrome
1631
+ importChrome: extractFromChrome,
1632
+ importBrave: extractFromBrave
1324
1633
  };
1325
1634
  }
1326
1635
 
@@ -1400,7 +1709,7 @@ function registerAuthCommand(input) {
1400
1709
  process.exitCode = 1;
1401
1710
  }
1402
1711
  });
1403
- auth.command("test").description("Verify credentials (calls Slack auth.test)").option("--workspace <url>", "Workspace URL (needed when you have multiple workspaces)").action(async (...args) => {
1712
+ auth.command("test").description("Verify credentials (calls Slack auth.test)").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when you have multiple workspaces)").action(async (...args) => {
1404
1713
  const [options] = args;
1405
1714
  try {
1406
1715
  const resp = await runAuthTest({
@@ -1436,6 +1745,29 @@ function registerAuthCommand(input) {
1436
1745
  process.exitCode = 1;
1437
1746
  }
1438
1747
  });
1748
+ auth.command("import-brave").description("Import xoxc/xoxd from a logged-in Slack tab in Brave Browser (macOS)").action(async () => {
1749
+ try {
1750
+ const extracted = await input.ctx.importBrave();
1751
+ if (!extracted) {
1752
+ throw new Error("Could not extract tokens from Brave. Open Slack in Brave and ensure you're logged in.");
1753
+ }
1754
+ for (const team of extracted.teams) {
1755
+ await upsertWorkspace({
1756
+ workspace_url: input.ctx.normalizeUrl(team.url),
1757
+ workspace_name: team.name,
1758
+ auth: {
1759
+ auth_type: "browser",
1760
+ xoxc_token: team.token,
1761
+ xoxd_cookie: extracted.cookie_d
1762
+ }
1763
+ });
1764
+ }
1765
+ console.log(`Imported ${extracted.teams.length} workspace token(s) from Brave.`);
1766
+ } catch (err) {
1767
+ console.error(input.ctx.errorMessage(err));
1768
+ process.exitCode = 1;
1769
+ }
1770
+ });
1439
1771
  auth.command("parse-curl").description("Paste a Slack API request copied as cURL (extracts xoxc/xoxd and saves locally)").action(async () => {
1440
1772
  try {
1441
1773
  const curlInput = await new Response(process.stdin).text();
@@ -1537,15 +1869,15 @@ function registerAuthCommand(input) {
1537
1869
 
1538
1870
  // src/slack/files.ts
1539
1871
  import { mkdir as mkdir3, writeFile as writeFile2 } from "node:fs/promises";
1540
- import { basename, join as join5, resolve } from "node:path";
1541
- import { existsSync as existsSync3 } from "node:fs";
1872
+ import { basename, join as join6, resolve } from "node:path";
1873
+ import { existsSync as existsSync4 } from "node:fs";
1542
1874
  async function downloadSlackFile(input) {
1543
1875
  const { auth, url, destDir, preferredName, options } = input;
1544
1876
  const absDir = resolve(destDir);
1545
1877
  await mkdir3(absDir, { recursive: true });
1546
1878
  const name = sanitizeFilename(preferredName || basename(new URL(url).pathname) || "file");
1547
- const path = join5(absDir, name);
1548
- if (existsSync3(path)) {
1879
+ const path = join6(absDir, name);
1880
+ if (existsSync4(path)) {
1549
1881
  return path;
1550
1882
  }
1551
1883
  const headers = {};
@@ -1600,27 +1932,27 @@ function extractTag(html, tag) {
1600
1932
  }
1601
1933
 
1602
1934
  // src/lib/tmp-paths.ts
1603
- import { join as join7, resolve as resolve2 } from "node:path";
1935
+ import { join as join8, resolve as resolve2 } from "node:path";
1604
1936
  import { mkdir as mkdir4 } from "node:fs/promises";
1605
1937
 
1606
1938
  // src/lib/app-dir.ts
1607
- import { homedir as homedir3, tmpdir } from "node:os";
1608
- import { join as join6 } from "node:path";
1939
+ import { homedir as homedir4, tmpdir } from "node:os";
1940
+ import { join as join7 } from "node:path";
1609
1941
  function getAppDir() {
1610
1942
  const xdg = process.env.XDG_RUNTIME_DIR?.trim();
1611
1943
  if (xdg) {
1612
- return join6(xdg, "agent-slack");
1944
+ return join7(xdg, "agent-slack");
1613
1945
  }
1614
- const home = homedir3();
1946
+ const home = homedir4();
1615
1947
  if (home) {
1616
- return join6(home, ".agent-slack");
1948
+ return join7(home, ".agent-slack");
1617
1949
  }
1618
- return join6(tmpdir(), "agent-slack");
1950
+ return join7(tmpdir(), "agent-slack");
1619
1951
  }
1620
1952
 
1621
1953
  // src/lib/tmp-paths.ts
1622
1954
  function getDownloadsDir() {
1623
- return resolve2(join7(getAppDir(), "tmp", "downloads"));
1955
+ return resolve2(join8(getAppDir(), "tmp", "downloads"));
1624
1956
  }
1625
1957
  async function ensureDownloadsDir() {
1626
1958
  const dir = getDownloadsDir();
@@ -1653,12 +1985,12 @@ function parseSlackCanvasUrl(input) {
1653
1985
  }
1654
1986
  async function fetchCanvasMarkdown(client, input) {
1655
1987
  const info = await client.api("files.info", { file: input.canvasId });
1656
- const file = isRecord6(info.file) ? info.file : null;
1988
+ const file = isRecord5(info.file) ? info.file : null;
1657
1989
  if (!file) {
1658
1990
  throw new Error("Canvas not found (files.info returned no file)");
1659
1991
  }
1660
- const title = (getString2(file.title) || getString2(file.name) || "").trim() || undefined;
1661
- const downloadUrl = getString2(file.url_private_download) ?? getString2(file.url_private);
1992
+ const title = (getString(file.title) || getString(file.name) || "").trim() || undefined;
1993
+ const downloadUrl = getString(file.url_private_download) ?? getString(file.url_private);
1662
1994
  if (!downloadUrl) {
1663
1995
  throw new Error("Canvas has no download URL");
1664
1996
  }
@@ -1700,17 +2032,11 @@ async function fetchCanvasMarkdown(client, input) {
1700
2032
  }
1701
2033
  };
1702
2034
  }
1703
- function isRecord6(value) {
1704
- return typeof value === "object" && value !== null;
1705
- }
1706
- function getString2(value) {
1707
- return typeof value === "string" ? value : undefined;
1708
- }
1709
2035
 
1710
2036
  // src/cli/canvas-command.ts
1711
2037
  function registerCanvasCommand(input) {
1712
2038
  const canvasCmd = input.program.command("canvas").description("Work with Slack canvases");
1713
- canvasCmd.command("get").description("Fetch a Slack canvas and convert it to Markdown").argument("<canvas>", "Slack canvas URL (…/docs/…/F…) or canvas id (F…)").option("--workspace <url>", "Workspace URL (required if passing a canvas id and you have multiple workspaces)").option("--max-chars <n>", "Max markdown characters to include (default 20000, -1 for unlimited)", "20000").action(async (...args) => {
2039
+ canvasCmd.command("get").description("Fetch a Slack canvas and convert it to Markdown").argument("<canvas>", "Slack canvas URL (…/docs/…/F…) or canvas id (F…)").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if passing a canvas id across multiple workspaces)").option("--max-chars <n>", "Max markdown characters to include (default 20000, -1 for unlimited)", "20000").action(async (...args) => {
1714
2040
  const [value, options] = args;
1715
2041
  try {
1716
2042
  let workspaceUrl;
@@ -1792,17 +2118,21 @@ function slackMrkdwnToMarkdown(text) {
1792
2118
  }
1793
2119
 
1794
2120
  // src/slack/render.ts
2121
+ var MAX_ATTACHMENT_DEPTH = 8;
1795
2122
  function renderSlackMessageContent(msg) {
1796
2123
  const msgObj = isRecord7(msg) ? msg : {};
1797
2124
  const blockMrkdwn = extractMrkdwnFromBlocks(msgObj.blocks);
1798
- if (blockMrkdwn.trim()) {
1799
- return slackMrkdwnToMarkdown(blockMrkdwn).trim();
1800
- }
1801
- const attachmentMrkdwn = extractMrkdwnFromAttachments(msgObj.attachments);
1802
- if (attachmentMrkdwn.trim()) {
1803
- return slackMrkdwnToMarkdown(attachmentMrkdwn).trim();
2125
+ const attachmentMrkdwn = extractMrkdwnFromAttachments(msgObj.attachments, {
2126
+ depth: 0,
2127
+ seen: new WeakSet
2128
+ });
2129
+ const combined = [blockMrkdwn.trim(), attachmentMrkdwn.trim()].filter(Boolean).join(`
2130
+
2131
+ `);
2132
+ if (combined) {
2133
+ return slackMrkdwnToMarkdown(combined).trim();
1804
2134
  }
1805
- const text = getString3(msgObj.text).trim();
2135
+ const text = getString2(msgObj.text).trim();
1806
2136
  if (text) {
1807
2137
  return slackMrkdwnToMarkdown(text).trim();
1808
2138
  }
@@ -1817,28 +2147,28 @@ function extractMrkdwnFromBlocks(blocks) {
1817
2147
  if (!isRecord7(b)) {
1818
2148
  continue;
1819
2149
  }
1820
- const type = getString3(b.type);
2150
+ const type = getString2(b.type);
1821
2151
  if (type === "section") {
1822
2152
  const text = isRecord7(b.text) ? b.text : null;
1823
- const textType = text ? getString3(text.type) : "";
2153
+ const textType = text ? getString2(text.type) : "";
1824
2154
  if (textType === "mrkdwn" || textType === "plain_text") {
1825
- out.push(getString3(text?.text));
2155
+ out.push(getString2(text?.text));
1826
2156
  }
1827
2157
  if (Array.isArray(b.fields)) {
1828
2158
  for (const f of b.fields) {
1829
2159
  if (!isRecord7(f)) {
1830
2160
  continue;
1831
2161
  }
1832
- const fieldType = getString3(f.type);
2162
+ const fieldType = getString2(f.type);
1833
2163
  if (fieldType === "mrkdwn" || fieldType === "plain_text") {
1834
- out.push(getString3(f.text));
2164
+ out.push(getString2(f.text));
1835
2165
  }
1836
2166
  }
1837
2167
  }
1838
2168
  const accessory = isRecord7(b.accessory) ? b.accessory : null;
1839
- if (getString3(accessory?.type) === "button") {
1840
- const label = getString3(accessory?.text?.text);
1841
- const url = getString3(accessory?.url);
2169
+ if (getString2(accessory?.type) === "button") {
2170
+ const label = getString2(accessory?.text?.text);
2171
+ const url = getString2(accessory?.url);
1842
2172
  if (url) {
1843
2173
  out.push(label ? `${label}: ${url}` : url);
1844
2174
  }
@@ -1850,9 +2180,9 @@ function extractMrkdwnFromBlocks(blocks) {
1850
2180
  if (!isRecord7(el)) {
1851
2181
  continue;
1852
2182
  }
1853
- if (getString3(el.type) === "button") {
1854
- const label = getString3(el.text?.text);
1855
- const url = getString3(el.url);
2183
+ if (getString2(el.type) === "button") {
2184
+ const label = getString2(el.text?.text);
2185
+ const url = getString2(el.url);
1856
2186
  if (url) {
1857
2187
  out.push(label ? `${label}: ${url}` : url);
1858
2188
  }
@@ -1865,19 +2195,19 @@ function extractMrkdwnFromBlocks(blocks) {
1865
2195
  if (!isRecord7(el)) {
1866
2196
  continue;
1867
2197
  }
1868
- const elType = getString3(el.type);
2198
+ const elType = getString2(el.type);
1869
2199
  if (elType === "mrkdwn") {
1870
- out.push(getString3(el.text));
2200
+ out.push(getString2(el.text));
1871
2201
  }
1872
2202
  if (elType === "plain_text") {
1873
- out.push(getString3(el.text));
2203
+ out.push(getString2(el.text));
1874
2204
  }
1875
2205
  }
1876
2206
  continue;
1877
2207
  }
1878
2208
  if (type === "image") {
1879
- const alt = getString3(b.alt_text);
1880
- const url = getString3(b.image_url);
2209
+ const alt = getString2(b.alt_text);
2210
+ const url = getString2(b.image_url);
1881
2211
  if (url) {
1882
2212
  out.push(alt ? `${alt}: ${url}` : url);
1883
2213
  }
@@ -1915,7 +2245,7 @@ function extractMrkdwnFromRichTextElement(el) {
1915
2245
  if (!isRecord7(el)) {
1916
2246
  return "";
1917
2247
  }
1918
- const t = getString3(el.type);
2248
+ const t = getString2(el.type);
1919
2249
  if (t === "rich_text_section") {
1920
2250
  const parts = [];
1921
2251
  for (const child of Array.isArray(el.elements) ? el.elements : []) {
@@ -1961,7 +2291,7 @@ function extractMrkdwnFromRichTextElement(el) {
1961
2291
  `);
1962
2292
  }
1963
2293
  if (t === "text") {
1964
- const raw = getString3(el.text);
2294
+ const raw = getString2(el.text);
1965
2295
  const style = isRecord7(el.style) ? el.style : null;
1966
2296
  if (!style) {
1967
2297
  return raw;
@@ -1982,28 +2312,31 @@ function extractMrkdwnFromRichTextElement(el) {
1982
2312
  return text;
1983
2313
  }
1984
2314
  if (t === "link") {
1985
- const url = getString3(el.url);
1986
- const text = getString3(el.text);
2315
+ const url = getString2(el.url);
2316
+ const text = getString2(el.text);
1987
2317
  if (!url) {
1988
2318
  return text;
1989
2319
  }
1990
2320
  return text ? `<${url}|${text}>` : url;
1991
2321
  }
1992
2322
  if (t === "emoji") {
1993
- const name = getString3(el.name);
2323
+ const name = getString2(el.name);
1994
2324
  return name ? `:${name}:` : "";
1995
2325
  }
1996
2326
  if (t === "user") {
1997
- const userId = getString3(el.user_id);
2327
+ const userId = getString2(el.user_id);
1998
2328
  return userId ? `<@${userId}>` : "";
1999
2329
  }
2000
2330
  if (t === "channel") {
2001
- const channelId = getString3(el.channel_id);
2331
+ const channelId = getString2(el.channel_id);
2002
2332
  return channelId ? `<#${channelId}>` : "";
2003
2333
  }
2004
2334
  return "";
2005
2335
  }
2006
- function extractMrkdwnFromAttachments(attachments) {
2336
+ function extractMrkdwnFromAttachments(attachments, state) {
2337
+ if (state.depth >= MAX_ATTACHMENT_DEPTH) {
2338
+ return "";
2339
+ }
2007
2340
  if (!Array.isArray(attachments)) {
2008
2341
  return "";
2009
2342
  }
@@ -2012,17 +2345,34 @@ function extractMrkdwnFromAttachments(attachments) {
2012
2345
  if (!isRecord7(a)) {
2013
2346
  continue;
2014
2347
  }
2348
+ if (state.seen.has(a)) {
2349
+ continue;
2350
+ }
2351
+ state.seen.add(a);
2352
+ const isSharedMessage = Boolean(a.is_share || a.is_msg_unfurl && Array.isArray(a.message_blocks));
2015
2353
  const chunk = [];
2354
+ if (isSharedMessage) {
2355
+ chunk.push(formatForwardHeader(a));
2356
+ const body = extractForwardedMessageBody(a, state).trim() || extractMrkdwnFromAttachments(a.attachments, nextState(state)).trim() || getString2(a.text).trim();
2357
+ if (body) {
2358
+ chunk.push(quoteMarkdown(body));
2359
+ }
2360
+ if (chunk.length > 0) {
2361
+ parts.push(chunk.join(`
2362
+ `));
2363
+ }
2364
+ continue;
2365
+ }
2016
2366
  const blocks = extractMrkdwnFromBlocks(a.blocks);
2017
2367
  if (blocks.trim()) {
2018
2368
  chunk.push(blocks);
2019
2369
  }
2020
- const pretext = getString3(a.pretext);
2370
+ const pretext = getString2(a.pretext);
2021
2371
  if (pretext) {
2022
2372
  chunk.push(pretext);
2023
2373
  }
2024
- const title = getString3(a.title);
2025
- const titleLink = getString3(a.title_link);
2374
+ const title = getString2(a.title);
2375
+ const titleLink = getString2(a.title_link);
2026
2376
  if (titleLink && title) {
2027
2377
  chunk.push(`<${titleLink}|${title}>`);
2028
2378
  } else if (title) {
@@ -2030,7 +2380,7 @@ function extractMrkdwnFromAttachments(attachments) {
2030
2380
  } else if (titleLink) {
2031
2381
  chunk.push(titleLink);
2032
2382
  }
2033
- const text = getString3(a.text);
2383
+ const text = getString2(a.text);
2034
2384
  if (text) {
2035
2385
  chunk.push(text);
2036
2386
  }
@@ -2039,8 +2389,8 @@ function extractMrkdwnFromAttachments(attachments) {
2039
2389
  if (!isRecord7(f)) {
2040
2390
  continue;
2041
2391
  }
2042
- const fieldTitle = getString3(f.title);
2043
- const value = getString3(f.value);
2392
+ const fieldTitle = getString2(f.title);
2393
+ const value = getString2(f.value);
2044
2394
  if (fieldTitle && value) {
2045
2395
  chunk.push(`${fieldTitle}
2046
2396
  ${value}`);
@@ -2051,26 +2401,174 @@ ${value}`);
2051
2401
  }
2052
2402
  }
2053
2403
  }
2054
- const fallback = getString3(a.fallback);
2404
+ const fallback = getString2(a.fallback);
2055
2405
  if (chunk.length === 0 && fallback) {
2056
2406
  chunk.push(fallback);
2057
2407
  }
2408
+ const nestedAttachments = extractMrkdwnFromAttachments(a.attachments, nextState(state));
2409
+ if (nestedAttachments.trim()) {
2410
+ chunk.push(nestedAttachments);
2411
+ }
2058
2412
  if (chunk.length > 0) {
2059
- parts.push(chunk.join(`
2413
+ parts.push(uniqueTexts(chunk).join(`
2060
2414
  `));
2061
2415
  }
2062
2416
  }
2063
- return parts.join(`
2417
+ return uniqueTexts(parts).join(`
2418
+
2419
+ `);
2420
+ }
2421
+ function formatForwardHeader(a) {
2422
+ const authorName = getString2(a.author_name);
2423
+ const authorLink = getString2(a.author_link);
2424
+ const fromUrl = getString2(a.from_url);
2425
+ const authorPart = authorName && authorLink ? `<${authorLink}|${authorName}>` : authorName || "";
2426
+ const sourcePart = fromUrl ? `<${fromUrl}|original>` : "";
2427
+ if (authorPart && sourcePart) {
2428
+ return `*Forwarded from ${authorPart} | ${sourcePart}*`;
2429
+ }
2430
+ if (authorPart) {
2431
+ return `*Forwarded from ${authorPart}*`;
2432
+ }
2433
+ if (sourcePart) {
2434
+ return `*Forwarded message | ${sourcePart}*`;
2435
+ }
2436
+ return "*Forwarded message*";
2437
+ }
2438
+ function extractForwardedMessageBody(attachment, state) {
2439
+ const messageBlocks = attachment.message_blocks;
2440
+ const topLevelFiles = extractFileMentions(attachment.files).trim();
2441
+ if (!Array.isArray(messageBlocks)) {
2442
+ return topLevelFiles;
2443
+ }
2444
+ const out = [];
2445
+ for (const mb of messageBlocks) {
2446
+ if (!isRecord7(mb)) {
2447
+ continue;
2448
+ }
2449
+ const message = isRecord7(mb.message) ? mb.message : null;
2450
+ if (!message) {
2451
+ continue;
2452
+ }
2453
+ const messageText = getString2(message.text).trim();
2454
+ const blocksContent = extractMrkdwnFromBlocks(message.blocks).trim();
2455
+ const attachmentsContent = extractMrkdwnFromAttachments(message.attachments, nextState(state)).trim();
2456
+ const fileMentions = extractFileMentions(message.files).trim();
2457
+ const content = uniqueTexts([
2458
+ blocksContent,
2459
+ attachmentsContent,
2460
+ messageText,
2461
+ fileMentions
2462
+ ]).join(`
2064
2463
 
2464
+ `);
2465
+ if (content) {
2466
+ out.push(content);
2467
+ }
2468
+ }
2469
+ return uniqueTexts([topLevelFiles, ...out]).join(`
2470
+ `);
2471
+ }
2472
+ function nextState(state) {
2473
+ return { depth: state.depth + 1, seen: state.seen };
2474
+ }
2475
+ function quoteMarkdown(text) {
2476
+ return text.split(`
2477
+ `).map((line) => `> ${line}`).join(`
2478
+ `);
2479
+ }
2480
+ function uniqueTexts(values) {
2481
+ const out = [];
2482
+ const seen = new Set;
2483
+ for (const value of values) {
2484
+ const text = value.trim();
2485
+ if (!text || seen.has(text)) {
2486
+ continue;
2487
+ }
2488
+ seen.add(text);
2489
+ out.push(text);
2490
+ }
2491
+ return out;
2492
+ }
2493
+ function extractFileMentions(files) {
2494
+ if (!Array.isArray(files)) {
2495
+ return "";
2496
+ }
2497
+ const lines = [];
2498
+ for (const f of files) {
2499
+ if (!isRecord7(f)) {
2500
+ continue;
2501
+ }
2502
+ const name = getString2(f.title) || getString2(f.name) || "file";
2503
+ const url = getString2(f.permalink) || getString2(f.url_private_download) || getString2(f.url_private);
2504
+ if (url) {
2505
+ lines.push(`<${url}|${name}>`);
2506
+ continue;
2507
+ }
2508
+ lines.push(name);
2509
+ }
2510
+ return uniqueTexts(lines).join(`
2065
2511
  `);
2066
2512
  }
2067
2513
  function isRecord7(value) {
2068
2514
  return typeof value === "object" && value !== null;
2069
2515
  }
2070
- function getString3(value) {
2516
+ function getString2(value) {
2071
2517
  return typeof value === "string" ? value : "";
2072
2518
  }
2073
2519
 
2520
+ // src/slack/message-api-parsing.ts
2521
+ function toSlackFileSummary(value) {
2522
+ if (!isRecord5(value)) {
2523
+ return null;
2524
+ }
2525
+ const id = getString(value.id);
2526
+ if (!id) {
2527
+ return null;
2528
+ }
2529
+ return {
2530
+ id,
2531
+ name: getString(value.name),
2532
+ title: getString(value.title),
2533
+ mimetype: getString(value.mimetype),
2534
+ filetype: getString(value.filetype),
2535
+ mode: getString(value.mode),
2536
+ permalink: getString(value.permalink),
2537
+ url_private: getString(value.url_private),
2538
+ url_private_download: getString(value.url_private_download),
2539
+ size: getNumber(value.size)
2540
+ };
2541
+ }
2542
+ async function enrichFiles(client, files) {
2543
+ const out = [];
2544
+ for (const f of files) {
2545
+ if (f.mode === "snippet" || !f.url_private_download) {
2546
+ try {
2547
+ const info = await client.api("files.info", { file: f.id });
2548
+ const file = isRecord5(info.file) ? info.file : null;
2549
+ out.push({
2550
+ ...f,
2551
+ name: f.name ?? getString(file?.name),
2552
+ title: f.title ?? getString(file?.title),
2553
+ mimetype: f.mimetype ?? getString(file?.mimetype),
2554
+ filetype: f.filetype ?? getString(file?.filetype),
2555
+ mode: f.mode ?? getString(file?.mode),
2556
+ permalink: f.permalink ?? getString(file?.permalink),
2557
+ url_private: f.url_private ?? getString(file?.url_private),
2558
+ url_private_download: f.url_private_download ?? getString(file?.url_private_download),
2559
+ snippet: {
2560
+ content: getString(file?.content),
2561
+ language: getString(file?.filetype)
2562
+ }
2563
+ });
2564
+ continue;
2565
+ } catch {}
2566
+ }
2567
+ out.push(f);
2568
+ }
2569
+ return out;
2570
+ }
2571
+
2074
2572
  // src/slack/messages.ts
2075
2573
  function toCompactMessage(msg, input) {
2076
2574
  const maxBodyChars = input?.maxBodyChars ?? 8000;
@@ -2096,7 +2594,8 @@ function toCompactMessage(msg, input) {
2096
2594
  author: msg.user || msg.bot_id ? { user_id: msg.user, bot_id: msg.bot_id } : undefined,
2097
2595
  content: content ? content : undefined,
2098
2596
  files: files && files.length > 0 ? files : undefined,
2099
- reactions: includeReactions ? compactReactions(msg.reactions) : undefined
2597
+ reactions: includeReactions ? compactReactions(msg.reactions) : undefined,
2598
+ forwarded_threads: extractForwardedThreads(msg.attachments)
2100
2599
  };
2101
2600
  }
2102
2601
  function compactReactions(reactions) {
@@ -2105,10 +2604,10 @@ function compactReactions(reactions) {
2105
2604
  }
2106
2605
  const out = [];
2107
2606
  for (const r of reactions) {
2108
- if (!isRecord8(r)) {
2607
+ if (!isRecord5(r)) {
2109
2608
  continue;
2110
2609
  }
2111
- const name = getString4(r.name)?.trim() ?? "";
2610
+ const name = getString(r.name)?.trim() ?? "";
2112
2611
  if (!name) {
2113
2612
  continue;
2114
2613
  }
@@ -2118,6 +2617,46 @@ function compactReactions(reactions) {
2118
2617
  }
2119
2618
  return out.length ? out : undefined;
2120
2619
  }
2620
+ function extractForwardedThreads(attachments) {
2621
+ if (!Array.isArray(attachments) || attachments.length === 0) {
2622
+ return;
2623
+ }
2624
+ const out = [];
2625
+ const seen = new Set;
2626
+ for (const attachment of attachments) {
2627
+ if (!isRecord5(attachment)) {
2628
+ continue;
2629
+ }
2630
+ const fromUrl = getString(attachment.from_url)?.trim();
2631
+ if (!fromUrl) {
2632
+ continue;
2633
+ }
2634
+ let parsed;
2635
+ try {
2636
+ parsed = new URL(fromUrl);
2637
+ } catch {
2638
+ continue;
2639
+ }
2640
+ const threadTs = parsed.searchParams.get("thread_ts")?.trim();
2641
+ if (!threadTs || !/^\d{6,}\.\d{6}$/.test(threadTs)) {
2642
+ continue;
2643
+ }
2644
+ const channelId = parsed.searchParams.get("cid")?.trim();
2645
+ const replyCount = getNumber(attachment.reply_count);
2646
+ const key = `${fromUrl}::${threadTs}`;
2647
+ if (seen.has(key)) {
2648
+ continue;
2649
+ }
2650
+ seen.add(key);
2651
+ out.push({
2652
+ url: fromUrl,
2653
+ thread_ts: threadTs,
2654
+ channel_id: channelId || undefined,
2655
+ reply_count: replyCount
2656
+ });
2657
+ }
2658
+ return out.length ? out : undefined;
2659
+ }
2121
2660
  async function fetchMessage(client, input) {
2122
2661
  const history = await client.api("conversations.history", {
2123
2662
  channel: input.ref.channel_id,
@@ -2126,8 +2665,8 @@ async function fetchMessage(client, input) {
2126
2665
  limit: 5,
2127
2666
  include_all_metadata: input.includeReactions ? true : undefined
2128
2667
  });
2129
- const historyMessages = asArray2(history.messages);
2130
- let msg = historyMessages.find((m) => isRecord8(m) && getString4(m.ts) === input.ref.message_ts);
2668
+ const historyMessages = asArray(history.messages);
2669
+ let msg = historyMessages.find((m) => isRecord5(m) && getString(m.ts) === input.ref.message_ts);
2131
2670
  if (!msg && input.ref.thread_ts_hint) {
2132
2671
  msg = await findMessageInThread(client, {
2133
2672
  channelId: input.ref.channel_id,
@@ -2144,8 +2683,8 @@ async function fetchMessage(client, input) {
2144
2683
  limit: 1,
2145
2684
  include_all_metadata: input.includeReactions ? true : undefined
2146
2685
  });
2147
- const [root] = asArray2(rootResp.messages);
2148
- if (isRecord8(root) && getString4(root.ts) === input.ref.message_ts) {
2686
+ const [root] = asArray(rootResp.messages);
2687
+ if (isRecord5(root) && getString(root.ts) === input.ref.message_ts) {
2149
2688
  msg = root;
2150
2689
  }
2151
2690
  } catch {}
@@ -2153,20 +2692,20 @@ async function fetchMessage(client, input) {
2153
2692
  if (!msg) {
2154
2693
  throw new Error("Message not found (no access or wrong URL)");
2155
2694
  }
2156
- const files = asArray2(msg.files).map((f) => toSlackFileSummary(f)).filter((f) => f !== null);
2695
+ const files = asArray(msg.files).map((f) => toSlackFileSummary(f)).filter((f) => f !== null);
2157
2696
  const enrichedFiles = files.length > 0 ? await enrichFiles(client, files) : undefined;
2158
- const text = getString4(msg.text) ?? "";
2159
- const ts = getString4(msg.ts) ?? input.ref.message_ts;
2697
+ const text = getString(msg.text) ?? "";
2698
+ const ts = getString(msg.ts) ?? input.ref.message_ts;
2160
2699
  const blocks = Array.isArray(msg.blocks) ? msg.blocks : undefined;
2161
2700
  const attachments = Array.isArray(msg.attachments) ? msg.attachments : undefined;
2162
2701
  const reactions = Array.isArray(msg.reactions) ? msg.reactions : undefined;
2163
2702
  return {
2164
2703
  channel_id: input.ref.channel_id,
2165
2704
  ts,
2166
- thread_ts: getString4(msg.thread_ts),
2705
+ thread_ts: getString(msg.thread_ts),
2167
2706
  reply_count: getNumber(msg.reply_count),
2168
- user: getString4(msg.user),
2169
- bot_id: getString4(msg.bot_id),
2707
+ user: getString(msg.user),
2708
+ bot_id: getString(msg.bot_id),
2170
2709
  text,
2171
2710
  markdown: slackMrkdwnToMarkdown(text),
2172
2711
  blocks,
@@ -2185,19 +2724,105 @@ async function findMessageInThread(client, input) {
2185
2724
  cursor,
2186
2725
  include_all_metadata: input.includeReactions ? true : undefined
2187
2726
  });
2188
- const messages = asArray2(resp.messages);
2189
- const found = messages.find((m) => isRecord8(m) && getString4(m.ts) === input.targetTs);
2727
+ const messages = asArray(resp.messages);
2728
+ const found = messages.find((m) => isRecord5(m) && getString(m.ts) === input.targetTs);
2190
2729
  if (found) {
2191
2730
  return found;
2192
2731
  }
2193
- const meta = isRecord8(resp.response_metadata) ? resp.response_metadata : null;
2194
- const next = meta ? getString4(meta.next_cursor) : undefined;
2195
- if (!next) {
2732
+ const meta = isRecord5(resp.response_metadata) ? resp.response_metadata : null;
2733
+ const next = meta ? getString(meta.next_cursor) : undefined;
2734
+ if (!next) {
2735
+ break;
2736
+ }
2737
+ cursor = next;
2738
+ }
2739
+ return;
2740
+ }
2741
+ async function fetchChannelHistory(client, input) {
2742
+ const raw = input.limit ?? 25;
2743
+ const limit = Number.isFinite(raw) ? Math.min(Math.max(raw, 1), 200) : 25;
2744
+ const out = [];
2745
+ const withReactions = input.withReactions ?? [];
2746
+ const withoutReactions = input.withoutReactions ?? [];
2747
+ const hasReactionFilters = withReactions.length > 0 || withoutReactions.length > 0;
2748
+ const pageLimit = hasReactionFilters ? 200 : limit;
2749
+ let cursorLatest = input.latest;
2750
+ for (;; ) {
2751
+ const resp = await client.api("conversations.history", {
2752
+ channel: input.channelId,
2753
+ limit: pageLimit,
2754
+ latest: cursorLatest,
2755
+ oldest: input.oldest,
2756
+ include_all_metadata: input.includeReactions || hasReactionFilters ? true : undefined
2757
+ });
2758
+ const messages = asArray(resp.messages);
2759
+ if (messages.length === 0) {
2760
+ break;
2761
+ }
2762
+ for (const m of messages) {
2763
+ if (!isRecord5(m)) {
2764
+ continue;
2765
+ }
2766
+ if (hasReactionFilters && !passesReactionNameFilters(m, {
2767
+ withReactions,
2768
+ withoutReactions
2769
+ })) {
2770
+ continue;
2771
+ }
2772
+ const files = asArray(m.files).map((f) => toSlackFileSummary(f)).filter((f) => f !== null);
2773
+ const enrichedFiles = files.length > 0 ? await enrichFiles(client, files) : undefined;
2774
+ const text = getString(m.text) ?? "";
2775
+ out.push({
2776
+ channel_id: input.channelId,
2777
+ ts: getString(m.ts) ?? "",
2778
+ thread_ts: getString(m.thread_ts),
2779
+ reply_count: getNumber(m.reply_count),
2780
+ user: getString(m.user),
2781
+ bot_id: getString(m.bot_id),
2782
+ text,
2783
+ markdown: slackMrkdwnToMarkdown(text),
2784
+ blocks: Array.isArray(m.blocks) ? m.blocks : undefined,
2785
+ attachments: Array.isArray(m.attachments) ? m.attachments : undefined,
2786
+ files: enrichedFiles,
2787
+ reactions: Array.isArray(m.reactions) ? m.reactions : undefined
2788
+ });
2789
+ if (out.length >= limit) {
2790
+ break;
2791
+ }
2792
+ }
2793
+ if (out.length >= limit || !hasReactionFilters) {
2196
2794
  break;
2197
2795
  }
2198
- cursor = next;
2796
+ const last = messages.at(-1);
2797
+ const nextLatest = isRecord5(last) ? getString(last.ts) : undefined;
2798
+ if (!nextLatest || nextLatest === cursorLatest) {
2799
+ break;
2800
+ }
2801
+ cursorLatest = nextLatest;
2199
2802
  }
2200
- return;
2803
+ out.sort((a, b) => Number.parseFloat(a.ts) - Number.parseFloat(b.ts));
2804
+ return out;
2805
+ }
2806
+ function passesReactionNameFilters(msg, input) {
2807
+ const withReactions = input.withReactions ?? [];
2808
+ const withoutReactions = input.withoutReactions ?? [];
2809
+ const names = new Set;
2810
+ for (const r of asArray(msg.reactions)) {
2811
+ if (!isRecord5(r)) {
2812
+ continue;
2813
+ }
2814
+ const name = getString(r.name)?.trim();
2815
+ if (name) {
2816
+ names.add(name);
2817
+ }
2818
+ }
2819
+ if (withReactions.some((name) => !names.has(name))) {
2820
+ return false;
2821
+ }
2822
+ if (withoutReactions.some((name) => names.has(name))) {
2823
+ return false;
2824
+ }
2825
+ return true;
2201
2826
  }
2202
2827
  async function fetchThread(client, input) {
2203
2828
  const out = [];
@@ -2210,21 +2835,21 @@ async function fetchThread(client, input) {
2210
2835
  cursor,
2211
2836
  include_all_metadata: input.includeReactions ? true : undefined
2212
2837
  });
2213
- const messages = asArray2(resp.messages);
2838
+ const messages = asArray(resp.messages);
2214
2839
  for (const m of messages) {
2215
- if (!isRecord8(m)) {
2840
+ if (!isRecord5(m)) {
2216
2841
  continue;
2217
2842
  }
2218
- const files = asArray2(m.files).map((f) => toSlackFileSummary(f)).filter((f) => f !== null);
2843
+ const files = asArray(m.files).map((f) => toSlackFileSummary(f)).filter((f) => f !== null);
2219
2844
  const enrichedFiles = files.length > 0 ? await enrichFiles(client, files) : undefined;
2220
- const text = getString4(m.text) ?? "";
2845
+ const text = getString(m.text) ?? "";
2221
2846
  out.push({
2222
2847
  channel_id: input.channelId,
2223
- ts: getString4(m.ts) ?? "",
2224
- thread_ts: getString4(m.thread_ts),
2848
+ ts: getString(m.ts) ?? "",
2849
+ thread_ts: getString(m.thread_ts),
2225
2850
  reply_count: getNumber(m.reply_count),
2226
- user: getString4(m.user),
2227
- bot_id: getString4(m.bot_id),
2851
+ user: getString(m.user),
2852
+ bot_id: getString(m.bot_id),
2228
2853
  text,
2229
2854
  markdown: slackMrkdwnToMarkdown(text),
2230
2855
  blocks: Array.isArray(m.blocks) ? m.blocks : undefined,
@@ -2233,8 +2858,8 @@ async function fetchThread(client, input) {
2233
2858
  reactions: Array.isArray(m.reactions) ? m.reactions : undefined
2234
2859
  });
2235
2860
  }
2236
- const meta = isRecord8(resp.response_metadata) ? resp.response_metadata : null;
2237
- const next = meta ? getString4(meta.next_cursor) : undefined;
2861
+ const meta = isRecord5(resp.response_metadata) ? resp.response_metadata : null;
2862
+ const next = meta ? getString(meta.next_cursor) : undefined;
2238
2863
  if (!next) {
2239
2864
  break;
2240
2865
  }
@@ -2243,68 +2868,6 @@ async function fetchThread(client, input) {
2243
2868
  out.sort((a, b) => Number.parseFloat(a.ts) - Number.parseFloat(b.ts));
2244
2869
  return out;
2245
2870
  }
2246
- async function enrichFiles(client, files) {
2247
- const out = [];
2248
- for (const f of files) {
2249
- if (f.mode === "snippet" || !f.url_private_download) {
2250
- try {
2251
- const info = await client.api("files.info", { file: f.id });
2252
- const file = isRecord8(info.file) ? info.file : null;
2253
- out.push({
2254
- ...f,
2255
- name: f.name ?? getString4(file?.name),
2256
- title: f.title ?? getString4(file?.title),
2257
- mimetype: f.mimetype ?? getString4(file?.mimetype),
2258
- filetype: f.filetype ?? getString4(file?.filetype),
2259
- mode: f.mode ?? getString4(file?.mode),
2260
- permalink: f.permalink ?? getString4(file?.permalink),
2261
- url_private: f.url_private ?? getString4(file?.url_private),
2262
- url_private_download: f.url_private_download ?? getString4(file?.url_private_download),
2263
- snippet: {
2264
- content: getString4(file?.content),
2265
- language: getString4(file?.filetype)
2266
- }
2267
- });
2268
- continue;
2269
- } catch {}
2270
- }
2271
- out.push(f);
2272
- }
2273
- return out;
2274
- }
2275
- function isRecord8(value) {
2276
- return typeof value === "object" && value !== null;
2277
- }
2278
- function getString4(value) {
2279
- return typeof value === "string" ? value : undefined;
2280
- }
2281
- function getNumber(value) {
2282
- return typeof value === "number" ? value : undefined;
2283
- }
2284
- function asArray2(value) {
2285
- return Array.isArray(value) ? value : [];
2286
- }
2287
- function toSlackFileSummary(value) {
2288
- if (!isRecord8(value)) {
2289
- return null;
2290
- }
2291
- const id = getString4(value.id);
2292
- if (!id) {
2293
- return null;
2294
- }
2295
- return {
2296
- id,
2297
- name: getString4(value.name),
2298
- title: getString4(value.title),
2299
- mimetype: getString4(value.mimetype),
2300
- filetype: getString4(value.filetype),
2301
- mode: getString4(value.mode),
2302
- permalink: getString4(value.permalink),
2303
- url_private: getString4(value.url_private),
2304
- url_private_download: getString4(value.url_private_download),
2305
- size: getNumber(value.size)
2306
- };
2307
- }
2308
2871
 
2309
2872
  // src/slack/url.ts
2310
2873
  function parseSlackMessageUrl(input) {
@@ -2361,38 +2924,10 @@ function parseMsgTarget(input) {
2361
2924
  return { kind: "channel", channel: `#${trimmed}` };
2362
2925
  }
2363
2926
 
2364
- // src/cli/message-actions.ts
2365
- function isRecord9(value) {
2366
- return typeof value === "object" && value !== null && !Array.isArray(value);
2367
- }
2368
- function asArray3(value) {
2369
- return Array.isArray(value) ? value : [];
2370
- }
2371
- function getNumber2(value) {
2372
- return typeof value === "number" ? value : undefined;
2373
- }
2374
- async function getThreadSummary(client, input) {
2375
- const replyCount = input.msg.reply_count ?? 0;
2376
- const rootTs = input.msg.thread_ts ?? (replyCount > 0 ? input.msg.ts : null);
2377
- if (!rootTs) {
2378
- return null;
2379
- }
2380
- if (!input.msg.thread_ts && replyCount > 0) {
2381
- return { ts: rootTs, length: 1 + replyCount };
2382
- }
2383
- const resp = await client.api("conversations.replies", {
2384
- channel: input.channelId,
2385
- ts: rootTs,
2386
- limit: 1
2387
- });
2388
- const [root] = asArray3(isRecord9(resp) ? resp.messages : undefined);
2389
- const rootReplyCount = isRecord9(root) ? getNumber2(root.reply_count) : undefined;
2390
- if (rootReplyCount === undefined) {
2391
- return { ts: rootTs, length: 1 };
2392
- }
2393
- return { ts: rootTs, length: 1 + rootReplyCount };
2394
- }
2395
- function inferExt(file) {
2927
+ // src/cli/message-file-downloads.ts
2928
+ import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
2929
+ import { join as join9 } from "node:path";
2930
+ function inferFileExtension(file) {
2396
2931
  const mt = (file.mimetype || "").toLowerCase();
2397
2932
  const ft = (file.filetype || "").toLowerCase();
2398
2933
  if (mt === "image/png" || ft === "png") {
@@ -2417,42 +2952,148 @@ function inferExt(file) {
2417
2952
  return "json";
2418
2953
  }
2419
2954
  const name = file.name || file.title || "";
2420
- const m = name.match(/\.([A-Za-z0-9]{1,10})$/);
2421
- return m ? m[1].toLowerCase() : null;
2955
+ const match = name.match(/\.([A-Za-z0-9]{1,10})$/);
2956
+ return match ? match[1].toLowerCase() : null;
2957
+ }
2958
+ var CANVAS_MODES = new Set(["canvas", "quip", "docs"]);
2959
+ function looksLikeAuthPage(html) {
2960
+ return /<form[^>]+signin|data-qa="signin|<title>[^<]*Sign\s*in/i.test(html);
2961
+ }
2962
+ async function downloadCanvasAsMarkdown(input) {
2963
+ const htmlPath = await downloadSlackFile({
2964
+ auth: input.auth,
2965
+ url: input.url,
2966
+ destDir: input.destDir,
2967
+ preferredName: `${input.fileId}.html`,
2968
+ options: { allowHtml: true }
2969
+ });
2970
+ const html = await readFile4(htmlPath, "utf8");
2971
+ if (looksLikeAuthPage(html)) {
2972
+ throw new Error("Downloaded auth/login page instead of canvas content (token may be expired)");
2973
+ }
2974
+ const markdown = htmlToMarkdown(html).trim();
2975
+ const safeName = `${input.fileId.replace(/[\\/<>"|?*]/g, "_")}.md`;
2976
+ const markdownPath = join9(input.destDir, safeName);
2977
+ await writeFile3(markdownPath, markdown, "utf8");
2978
+ return markdownPath;
2422
2979
  }
2423
- async function downloadFilesForMessages(input) {
2980
+ async function downloadMessageFiles(input) {
2424
2981
  const downloadedPaths = {};
2425
2982
  const downloadsDir = await ensureDownloadsDir();
2426
- for (const m of input.messages) {
2427
- for (const f of m.files ?? []) {
2428
- if (downloadedPaths[f.id]) {
2983
+ for (const message of input.messages) {
2984
+ for (const file of message.files ?? []) {
2985
+ if (downloadedPaths[file.id]) {
2429
2986
  continue;
2430
2987
  }
2431
- const url = f.url_private_download || f.url_private;
2988
+ const isCanvas = file.mode != null && CANVAS_MODES.has(file.mode);
2989
+ const url = isCanvas ? file.url_private || file.url_private_download : file.url_private_download || file.url_private;
2432
2990
  if (!url) {
2433
2991
  continue;
2434
2992
  }
2435
- const ext = inferExt(f);
2436
- const path = await downloadSlackFile({
2437
- auth: input.auth,
2438
- url,
2439
- destDir: downloadsDir,
2440
- preferredName: `${f.id}${ext ? `.${ext}` : ""}`
2441
- });
2442
- downloadedPaths[f.id] = path;
2993
+ try {
2994
+ if (isCanvas) {
2995
+ downloadedPaths[file.id] = await downloadCanvasAsMarkdown({
2996
+ auth: input.auth,
2997
+ fileId: file.id,
2998
+ url,
2999
+ destDir: downloadsDir
3000
+ });
3001
+ } else {
3002
+ const ext = inferFileExtension(file);
3003
+ downloadedPaths[file.id] = await downloadSlackFile({
3004
+ auth: input.auth,
3005
+ url,
3006
+ destDir: downloadsDir,
3007
+ preferredName: `${file.id}${ext ? `.${ext}` : ""}`
3008
+ });
3009
+ }
3010
+ } catch (err) {
3011
+ console.error(`Warning: skipping file ${file.id}: ${err instanceof Error ? err.message : String(err)}`);
3012
+ }
2443
3013
  }
2444
3014
  }
2445
3015
  return downloadedPaths;
2446
3016
  }
3017
+
3018
+ // src/cli/message-url-warning.ts
3019
+ function warnOnTruncatedSlackUrl(ref) {
3020
+ if (ref.possiblyTruncated) {
3021
+ console.error(`Hint: URL may have been truncated by shell. Quote URLs containing "&":
3022
+ ` + ' agent-slack message get "https://...?thread_ts=...&cid=..."');
3023
+ }
3024
+ }
3025
+
3026
+ // src/cli/message-thread-info.ts
3027
+ function getNumber2(value) {
3028
+ return typeof value === "number" ? value : undefined;
3029
+ }
3030
+ async function getThreadSummary(client, input) {
3031
+ const replyCount = input.msg.reply_count ?? 0;
3032
+ const rootTs = input.msg.thread_ts ?? (replyCount > 0 ? input.msg.ts : null);
3033
+ if (!rootTs) {
3034
+ return null;
3035
+ }
3036
+ if (!input.msg.thread_ts && replyCount > 0) {
3037
+ return { ts: rootTs, length: 1 + replyCount };
3038
+ }
3039
+ const resp = await client.api("conversations.replies", {
3040
+ channel: input.channelId,
3041
+ ts: rootTs,
3042
+ limit: 1
3043
+ });
3044
+ const [root] = asArray(isRecord5(resp) ? resp.messages : undefined);
3045
+ const rootReplyCount = isRecord5(root) ? getNumber2(root.reply_count) : undefined;
3046
+ if (rootReplyCount === undefined) {
3047
+ return { ts: rootTs, length: 1 };
3048
+ }
3049
+ return { ts: rootTs, length: 1 + rootReplyCount };
3050
+ }
2447
3051
  function toThreadListMessage(m) {
2448
3052
  const { channel_id: _channelId, thread_ts: _threadTs, ...rest } = m;
2449
3053
  return rest;
2450
3054
  }
2451
- function warnIfTruncated(ref) {
2452
- if (ref.possiblyTruncated) {
2453
- console.error(`Hint: URL may have been truncated by shell. Quote URLs containing "&":
2454
- ` + ' agent-slack message get "https://...?thread_ts=...&cid=..."');
3055
+
3056
+ // src/cli/message-actions.ts
3057
+ function parseLimit(raw) {
3058
+ if (raw === undefined) {
3059
+ return;
3060
+ }
3061
+ const n = Number.parseInt(raw, 10);
3062
+ if (!Number.isFinite(n) || n < 1) {
3063
+ throw new Error(`Invalid --limit value "${raw}": must be a positive integer`);
3064
+ }
3065
+ return n;
3066
+ }
3067
+ function requireMessageTs(raw) {
3068
+ const ts = raw?.trim();
3069
+ if (!ts) {
3070
+ throw new Error('When targeting a channel, you must pass --ts "<seconds>.<micros>"');
3071
+ }
3072
+ return ts;
3073
+ }
3074
+ function parseReactionFilters(raw) {
3075
+ if (!Array.isArray(raw) || raw.length === 0) {
3076
+ return [];
3077
+ }
3078
+ const out = [];
3079
+ for (const value of raw) {
3080
+ const normalized = normalizeSlackReactionName(String(value));
3081
+ if (!out.includes(normalized)) {
3082
+ out.push(normalized);
3083
+ }
3084
+ }
3085
+ return out;
3086
+ }
3087
+ function requireOldestWhenReactionFiltersUsed(input) {
3088
+ const hasReactionFilters = input.withReactions.length > 0 || input.withoutReactions.length > 0;
3089
+ const oldest = input.oldest?.trim();
3090
+ if (!hasReactionFilters) {
3091
+ return oldest;
3092
+ }
3093
+ if (!oldest) {
3094
+ throw new Error('Reaction filters require --oldest "<seconds>.<micros>" to bound scan size. Example: --oldest "1770165109.628379"');
2455
3095
  }
3096
+ return oldest;
2456
3097
  }
2457
3098
  async function handleMessageGet(input) {
2458
3099
  const target = parseMsgTarget(input.targetInput);
@@ -2462,12 +3103,12 @@ async function handleMessageGet(input) {
2462
3103
  work: async () => {
2463
3104
  if (target.kind === "url") {
2464
3105
  const { ref: ref2 } = target;
2465
- warnIfTruncated(ref2);
3106
+ warnOnTruncatedSlackUrl(ref2);
2466
3107
  const { client: client2, auth: auth2 } = await input.ctx.getClientForWorkspace(ref2.workspace_url);
2467
3108
  const includeReactions2 = Boolean(input.options.includeReactions);
2468
3109
  const msg2 = await fetchMessage(client2, { ref: ref2, includeReactions: includeReactions2 });
2469
3110
  const thread2 = await getThreadSummary(client2, { channelId: ref2.channel_id, msg: msg2 });
2470
- const downloadedPaths2 = await downloadFilesForMessages({ auth: auth2, messages: [msg2] });
3111
+ const downloadedPaths2 = await downloadMessageFiles({ auth: auth2, messages: [msg2] });
2471
3112
  const maxBodyChars2 = Number.parseInt(input.options.maxBodyChars, 10);
2472
3113
  const message2 = toCompactMessage(msg2, { maxBodyChars: maxBodyChars2, includeReactions: includeReactions2, downloadedPaths: downloadedPaths2 });
2473
3114
  return pruneEmpty({ message: message2, thread: thread2 });
@@ -2492,7 +3133,7 @@ async function handleMessageGet(input) {
2492
3133
  };
2493
3134
  const msg = await fetchMessage(client, { ref, includeReactions });
2494
3135
  const thread = await getThreadSummary(client, { channelId, msg });
2495
- const downloadedPaths = await downloadFilesForMessages({ auth, messages: [msg] });
3136
+ const downloadedPaths = await downloadMessageFiles({ auth, messages: [msg] });
2496
3137
  const maxBodyChars = Number.parseInt(input.options.maxBodyChars, 10);
2497
3138
  const message = toCompactMessage(msg, { maxBodyChars, includeReactions, downloadedPaths });
2498
3139
  return pruneEmpty({ message, thread });
@@ -2505,9 +3146,15 @@ async function handleMessageList(input) {
2505
3146
  return input.ctx.withAutoRefresh({
2506
3147
  workspaceUrl: target.kind === "url" ? target.ref.workspace_url : workspaceUrl,
2507
3148
  work: async () => {
3149
+ const withReactions = parseReactionFilters(input.options.withReaction);
3150
+ const withoutReactions = parseReactionFilters(input.options.withoutReaction);
3151
+ const hasReactionFilters = withReactions.length > 0 || withoutReactions.length > 0;
2508
3152
  if (target.kind === "url") {
3153
+ if (hasReactionFilters) {
3154
+ throw new Error("Reaction filters are only supported for channel history mode (not message URL thread mode)");
3155
+ }
2509
3156
  const { ref } = target;
2510
- warnIfTruncated(ref);
3157
+ warnOnTruncatedSlackUrl(ref);
2511
3158
  const { client: client2, auth: auth2 } = await input.ctx.getClientForWorkspace(ref.workspace_url);
2512
3159
  const includeReactions2 = Boolean(input.options.includeReactions);
2513
3160
  const msg = await fetchMessage(client2, { ref, includeReactions: includeReactions2 });
@@ -2517,7 +3164,7 @@ async function handleMessageList(input) {
2517
3164
  threadTs: rootTs2,
2518
3165
  includeReactions: includeReactions2
2519
3166
  });
2520
- const downloadedPaths2 = await downloadFilesForMessages({ auth: auth2, messages: threadMessages2 });
3167
+ const downloadedPaths2 = await downloadMessageFiles({ auth: auth2, messages: threadMessages2 });
2521
3168
  const maxBodyChars2 = Number.parseInt(input.options.maxBodyChars, 10);
2522
3169
  return pruneEmpty({
2523
3170
  messages: threadMessages2.map((m) => toCompactMessage(m, { maxBodyChars: maxBodyChars2, includeReactions: includeReactions2, downloadedPaths: downloadedPaths2 })).map(toThreadListMessage)
@@ -2532,7 +3179,31 @@ async function handleMessageList(input) {
2532
3179
  const threadTs = input.options.threadTs?.trim();
2533
3180
  const ts = input.options.ts?.trim();
2534
3181
  if (!threadTs && !ts) {
2535
- throw new Error('When targeting a channel, you must pass --thread-ts "<seconds>.<micros>" (or --ts to resolve a message to its thread)');
3182
+ const includeReactions2 = Boolean(input.options.includeReactions);
3183
+ const limit = parseLimit(input.options.limit);
3184
+ const oldest = requireOldestWhenReactionFiltersUsed({
3185
+ oldest: input.options.oldest,
3186
+ withReactions,
3187
+ withoutReactions
3188
+ });
3189
+ const channelMessages = await fetchChannelHistory(client, {
3190
+ channelId,
3191
+ limit,
3192
+ latest: input.options.latest?.trim(),
3193
+ oldest,
3194
+ includeReactions: includeReactions2 || hasReactionFilters,
3195
+ withReactions,
3196
+ withoutReactions
3197
+ });
3198
+ const downloadedPaths2 = await downloadMessageFiles({ auth, messages: channelMessages });
3199
+ const maxBodyChars2 = Number.parseInt(input.options.maxBodyChars, 10);
3200
+ return pruneEmpty({
3201
+ channel_id: channelId,
3202
+ messages: channelMessages.map((m) => toCompactMessage(m, { maxBodyChars: maxBodyChars2, includeReactions: includeReactions2, downloadedPaths: downloadedPaths2 }))
3203
+ });
3204
+ }
3205
+ if (hasReactionFilters) {
3206
+ throw new Error("Reaction filters are only supported for channel history mode (without --thread-ts/--ts)");
2536
3207
  }
2537
3208
  const rootTs = threadTs ?? await (async () => {
2538
3209
  const ref = {
@@ -2551,7 +3222,7 @@ async function handleMessageList(input) {
2551
3222
  threadTs: rootTs,
2552
3223
  includeReactions
2553
3224
  });
2554
- const downloadedPaths = await downloadFilesForMessages({ auth, messages: threadMessages });
3225
+ const downloadedPaths = await downloadMessageFiles({ auth, messages: threadMessages });
2555
3226
  const maxBodyChars = Number.parseInt(input.options.maxBodyChars, 10);
2556
3227
  return pruneEmpty({
2557
3228
  messages: threadMessages.map((m) => toCompactMessage(m, { maxBodyChars, includeReactions, downloadedPaths })).map(toThreadListMessage)
@@ -2563,7 +3234,7 @@ async function sendMessage(input) {
2563
3234
  const target = parseMsgTarget(String(input.targetInput));
2564
3235
  if (target.kind === "url") {
2565
3236
  const { ref } = target;
2566
- warnIfTruncated(ref);
3237
+ warnOnTruncatedSlackUrl(ref);
2567
3238
  await input.ctx.withAutoRefresh({
2568
3239
  workspaceUrl: ref.workspace_url,
2569
3240
  work: async () => {
@@ -2598,6 +3269,70 @@ async function sendMessage(input) {
2598
3269
  });
2599
3270
  return { ok: true };
2600
3271
  }
3272
+ async function editMessage(input) {
3273
+ const target = parseMsgTarget(String(input.targetInput));
3274
+ const workspaceUrl = input.ctx.effectiveWorkspaceUrl(input.options.workspace);
3275
+ await input.ctx.withAutoRefresh({
3276
+ workspaceUrl: target.kind === "url" ? target.ref.workspace_url : workspaceUrl,
3277
+ work: async () => {
3278
+ if (target.kind === "url") {
3279
+ const { ref } = target;
3280
+ warnOnTruncatedSlackUrl(ref);
3281
+ const { client: client2 } = await input.ctx.getClientForWorkspace(ref.workspace_url);
3282
+ await client2.api("chat.update", {
3283
+ channel: ref.channel_id,
3284
+ ts: ref.message_ts,
3285
+ text: input.text
3286
+ });
3287
+ return;
3288
+ }
3289
+ const ts = requireMessageTs(input.options.ts);
3290
+ await input.ctx.assertWorkspaceSpecifiedForChannelNames({
3291
+ workspaceUrl,
3292
+ channels: [target.channel]
3293
+ });
3294
+ const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
3295
+ const channelId = await resolveChannelId(client, target.channel);
3296
+ await client.api("chat.update", {
3297
+ channel: channelId,
3298
+ ts,
3299
+ text: input.text
3300
+ });
3301
+ }
3302
+ });
3303
+ return { ok: true };
3304
+ }
3305
+ async function deleteMessage(input) {
3306
+ const target = parseMsgTarget(String(input.targetInput));
3307
+ const workspaceUrl = input.ctx.effectiveWorkspaceUrl(input.options.workspace);
3308
+ await input.ctx.withAutoRefresh({
3309
+ workspaceUrl: target.kind === "url" ? target.ref.workspace_url : workspaceUrl,
3310
+ work: async () => {
3311
+ if (target.kind === "url") {
3312
+ const { ref } = target;
3313
+ warnOnTruncatedSlackUrl(ref);
3314
+ const { client: client2 } = await input.ctx.getClientForWorkspace(ref.workspace_url);
3315
+ await client2.api("chat.delete", {
3316
+ channel: ref.channel_id,
3317
+ ts: ref.message_ts
3318
+ });
3319
+ return;
3320
+ }
3321
+ const ts = requireMessageTs(input.options.ts);
3322
+ await input.ctx.assertWorkspaceSpecifiedForChannelNames({
3323
+ workspaceUrl,
3324
+ channels: [target.channel]
3325
+ });
3326
+ const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
3327
+ const channelId = await resolveChannelId(client, target.channel);
3328
+ await client.api("chat.delete", {
3329
+ channel: channelId,
3330
+ ts
3331
+ });
3332
+ }
3333
+ });
3334
+ return { ok: true };
3335
+ }
2601
3336
  async function reactOnTarget(input) {
2602
3337
  const target = parseMsgTarget(input.targetInput);
2603
3338
  const workspaceUrl = input.ctx.effectiveWorkspaceUrl(input.options?.workspace);
@@ -2606,7 +3341,7 @@ async function reactOnTarget(input) {
2606
3341
  work: async () => {
2607
3342
  if (target.kind === "url") {
2608
3343
  const { ref } = target;
2609
- warnIfTruncated(ref);
3344
+ warnOnTruncatedSlackUrl(ref);
2610
3345
  const { client: client2 } = await input.ctx.getClientForWorkspace(ref.workspace_url);
2611
3346
  const name2 = normalizeSlackReactionName(input.emoji);
2612
3347
  await client2.api(`reactions.${input.action}`, {
@@ -2616,10 +3351,7 @@ async function reactOnTarget(input) {
2616
3351
  });
2617
3352
  return;
2618
3353
  }
2619
- const ts = input.options?.ts?.trim();
2620
- if (!ts) {
2621
- throw new Error('When targeting a channel, you must pass --ts "<seconds>.<micros>"');
2622
- }
3354
+ const ts = requireMessageTs(input.options?.ts);
2623
3355
  await input.ctx.assertWorkspaceSpecifiedForChannelNames({
2624
3356
  workspaceUrl,
2625
3357
  channels: [target.channel]
@@ -2638,9 +3370,12 @@ async function reactOnTarget(input) {
2638
3370
  }
2639
3371
 
2640
3372
  // src/cli/message-command.ts
3373
+ function collectOptionValue(value, previous = []) {
3374
+ return [...previous, value];
3375
+ }
2641
3376
  function registerMessageCommand(input) {
2642
3377
  const messageCmd = input.program.command("message").description("Read/write Slack messages (token-efficient JSON)");
2643
- messageCmd.command("get", { isDefault: true }).description("Fetch a single Slack message (with thread summary if any)").argument("<target>", "Slack message URL, #channel, or channel ID").option("--workspace <url>", "Workspace URL (needed when using #channel/channel id and you have multiple workspaces)").option("--ts <ts>", "Message ts (required when using #channel/channel id)").option("--thread-ts <ts>", "Thread root ts hint (useful for thread permalinks)").option("--max-body-chars <n>", "Max content characters to include (default 8000, -1 for unlimited)", "8000").option("--include-reactions", "Include reactions + reacting users").action(async (...args) => {
3378
+ messageCmd.command("get", { isDefault: true }).description("Fetch a single Slack message (with thread summary if any)").argument("<target>", "Slack message URL, #channel, or channel ID").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--ts <ts>", "Message ts (required when using #channel/channel id)").option("--thread-ts <ts>", "Thread root ts hint (useful for thread permalinks)").option("--max-body-chars <n>", "Max content characters to include (default 8000, -1 for unlimited)", "8000").option("--include-reactions", "Include reactions + reacting users").action(async (...args) => {
2644
3379
  const [targetInput, options] = args;
2645
3380
  try {
2646
3381
  const payload = await handleMessageGet({ ctx: input.ctx, targetInput, options });
@@ -2650,7 +3385,7 @@ function registerMessageCommand(input) {
2650
3385
  process.exitCode = 1;
2651
3386
  }
2652
3387
  });
2653
- messageCmd.command("list").description("Fetch the full thread for a Slack message URL").argument("<target>", "Slack message URL, #channel, or channel ID").option("--workspace <url>", "Workspace URL (needed when using #channel/channel id and you have multiple workspaces)").option("--thread-ts <ts>", "Thread root ts (required when using #channel/channel id unless you pass --ts)").option("--ts <ts>", "Message ts (optional: resolve message to its thread)").option("--max-body-chars <n>", "Max content characters to include (default 8000, -1 for unlimited)", "8000").option("--include-reactions", "Include reactions + reacting users").action(async (...args) => {
3388
+ messageCmd.command("list").description("List recent channel messages, or fetch a full thread").argument("<target>", "Slack message URL, #channel, or channel ID").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--thread-ts <ts>", "Thread root ts (lists thread replies instead of channel history)").option("--ts <ts>", "Message ts (resolve message to its thread)").option("--limit <n>", "Max messages to return for channel history (default 25, max 200)").option("--oldest <ts>", "Only messages after this ts (channel history mode)").option("--latest <ts>", "Only messages before this ts (channel history mode)").option("--with-reaction <emoji>", "Only include messages with this reaction (repeatable; channel history mode; requires --oldest)", collectOptionValue, []).option("--without-reaction <emoji>", "Only include messages without this reaction (repeatable; channel history mode; requires --oldest)", collectOptionValue, []).option("--max-body-chars <n>", "Max content characters to include (default 8000, -1 for unlimited)", "8000").option("--include-reactions", "Include reactions + reacting users").action(async (...args) => {
2654
3389
  const [targetInput, options] = args;
2655
3390
  try {
2656
3391
  const payload = await handleMessageList({ ctx: input.ctx, targetInput, options });
@@ -2660,7 +3395,36 @@ function registerMessageCommand(input) {
2660
3395
  process.exitCode = 1;
2661
3396
  }
2662
3397
  });
2663
- messageCmd.command("send").description("Send a message (optionally into a thread)").argument("<target>", "Slack message URL, #name/name, or channel id").argument("<text>", "Message text to post").option("--workspace <url>", "Workspace URL (needed when using #channel/channel id and you have multiple workspaces)").option("--thread-ts <ts>", "Thread root ts to post into (optional)").action(async (...args) => {
3398
+ messageCmd.command("edit").description("Edit a message").argument("<target>", "Slack message URL, #channel, or channel ID").argument("<text>", "New message text").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--ts <ts>", "Message ts (required when using #channel/channel id)").action(async (...args) => {
3399
+ const [targetInput, text, options] = args;
3400
+ try {
3401
+ const payload = await editMessage({
3402
+ ctx: input.ctx,
3403
+ targetInput,
3404
+ text,
3405
+ options
3406
+ });
3407
+ console.log(JSON.stringify(payload, null, 2));
3408
+ } catch (err) {
3409
+ console.error(input.ctx.errorMessage(err));
3410
+ process.exitCode = 1;
3411
+ }
3412
+ });
3413
+ messageCmd.command("delete").description("Delete a message").argument("<target>", "Slack message URL, #channel, or channel ID").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--ts <ts>", "Message ts (required when using #channel/channel id)").action(async (...args) => {
3414
+ const [targetInput, options] = args;
3415
+ try {
3416
+ const payload = await deleteMessage({
3417
+ ctx: input.ctx,
3418
+ targetInput,
3419
+ options
3420
+ });
3421
+ console.log(JSON.stringify(payload, null, 2));
3422
+ } catch (err) {
3423
+ console.error(input.ctx.errorMessage(err));
3424
+ process.exitCode = 1;
3425
+ }
3426
+ });
3427
+ messageCmd.command("send").description("Send a message (optionally into a thread)").argument("<target>", "Slack message URL, #name/name, or channel id").argument("<text>", "Message text to post").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--thread-ts <ts>", "Thread root ts to post into (optional)").action(async (...args) => {
2664
3428
  const [targetInput, text, options] = args;
2665
3429
  try {
2666
3430
  const payload = await sendMessage({
@@ -2676,7 +3440,7 @@ function registerMessageCommand(input) {
2676
3440
  }
2677
3441
  });
2678
3442
  const reactCmd = messageCmd.command("react").description("Add or remove reactions");
2679
- reactCmd.command("add").description("Add a reaction to a message").argument("<target>", "Slack message URL, #channel, or channel ID").argument("<emoji>", "Emoji to react with (:rocket:, rocket, or \uD83D\uDE80)").option("--workspace <url>", "Workspace URL (needed when using #channel/channel id and you have multiple workspaces)").option("--ts <ts>", "Message ts (required when using #channel/channel id)").action(async (...args) => {
3443
+ reactCmd.command("add").description("Add a reaction to a message").argument("<target>", "Slack message URL, #channel, or channel ID").argument("<emoji>", "Emoji to react with (:rocket:, rocket, or \uD83D\uDE80)").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--ts <ts>", "Message ts (required when using #channel/channel id)").action(async (...args) => {
2680
3444
  const [targetInput, emoji2, options] = args;
2681
3445
  try {
2682
3446
  const payload = await reactOnTarget({
@@ -2692,7 +3456,7 @@ function registerMessageCommand(input) {
2692
3456
  process.exitCode = 1;
2693
3457
  }
2694
3458
  });
2695
- reactCmd.command("remove").description("Remove a reaction from a message").argument("<target>", "Slack message URL, #channel, or channel ID").argument("<emoji>", "Emoji to remove (:rocket:, rocket, or \uD83D\uDE80)").option("--workspace <url>", "Workspace URL (needed when using #channel/channel id and you have multiple workspaces)").option("--ts <ts>", "Message ts (required when using #channel/channel id)").action(async (...args) => {
3459
+ reactCmd.command("remove").description("Remove a reaction from a message").argument("<target>", "Slack message URL, #channel, or channel ID").argument("<emoji>", "Emoji to remove (:rocket:, rocket, or \uD83D\uDE80)").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--ts <ts>", "Message ts (required when using #channel/channel id)").action(async (...args) => {
2696
3460
  const [targetInput, emoji2, options] = args;
2697
3461
  try {
2698
3462
  const payload = await reactOnTarget({
@@ -2710,20 +3474,6 @@ function registerMessageCommand(input) {
2710
3474
  });
2711
3475
  }
2712
3476
 
2713
- // src/slack/search-guards.ts
2714
- function isRecord10(value) {
2715
- return typeof value === "object" && value !== null && !Array.isArray(value);
2716
- }
2717
- function asArray4(value) {
2718
- return Array.isArray(value) ? value : [];
2719
- }
2720
- function getString5(value) {
2721
- return typeof value === "string" ? value : undefined;
2722
- }
2723
- function getNumber3(value) {
2724
- return typeof value === "number" ? value : undefined;
2725
- }
2726
-
2727
3477
  // src/slack/search-query.ts
2728
3478
  async function buildSlackSearchQuery(client, input) {
2729
3479
  const parts = [];
@@ -2776,8 +3526,8 @@ async function userTokenForSearch(client, user) {
2776
3526
  if (/^U[A-Z0-9]{8,}$/.test(trimmed)) {
2777
3527
  try {
2778
3528
  const info = await client.api("users.info", { user: trimmed });
2779
- const infoUser = isRecord10(info) ? info.user : null;
2780
- const name = isRecord10(infoUser) ? (getString5(infoUser.name) ?? "").trim() : "";
3529
+ const infoUser = isRecord5(info) ? info.user : null;
3530
+ const name = isRecord5(infoUser) ? (getString(infoUser.name) ?? "").trim() : "";
2781
3531
  return name ? `from:@${name}` : null;
2782
3532
  } catch {
2783
3533
  return null;
@@ -2798,8 +3548,8 @@ async function channelTokenForSearch(client, channel) {
2798
3548
  const info = await client.api("conversations.info", {
2799
3549
  channel: normalized.value
2800
3550
  });
2801
- const channelInfo = isRecord10(info) ? info.channel : null;
2802
- const name = isRecord10(channelInfo) ? (getString5(channelInfo.name) ?? "").trim() : "";
3551
+ const channelInfo = isRecord5(info) ? info.channel : null;
3552
+ const name = isRecord5(channelInfo) ? (getString(channelInfo.name) ?? "").trim() : "";
2803
3553
  if (name) {
2804
3554
  return `in:#${name}`;
2805
3555
  }
@@ -2818,19 +3568,19 @@ async function resolveUserId(client, input) {
2818
3568
  let cursor;
2819
3569
  for (;; ) {
2820
3570
  const resp = await client.api("users.list", { limit: 200, cursor });
2821
- const members = isRecord10(resp) ? asArray4(resp.members).filter(isRecord10) : [];
3571
+ const members = isRecord5(resp) ? asArray(resp.members).filter(isRecord5) : [];
2822
3572
  const found = members.find((m) => {
2823
- const mName = getString5(m.name);
2824
- const profile = isRecord10(m.profile) ? m.profile : null;
2825
- const display = profile ? getString5(profile.display_name) : undefined;
3573
+ const mName = getString(m.name);
3574
+ const profile = isRecord5(m.profile) ? m.profile : null;
3575
+ const display = profile ? getString(profile.display_name) : undefined;
2826
3576
  return mName === name || display === name;
2827
3577
  });
2828
- const foundId = found ? getString5(found.id) : undefined;
3578
+ const foundId = found ? getString(found.id) : undefined;
2829
3579
  if (foundId) {
2830
3580
  return foundId;
2831
3581
  }
2832
- const meta = isRecord10(resp) ? resp.response_metadata : null;
2833
- const next = isRecord10(meta) ? getString5(meta.next_cursor) : undefined;
3582
+ const meta = isRecord5(resp) ? resp.response_metadata : null;
3583
+ const next = isRecord5(meta) ? getString(meta.next_cursor) : undefined;
2834
3584
  if (!next) {
2835
3585
  break;
2836
3586
  }
@@ -2854,11 +3604,11 @@ async function searchMessagesRaw(client, input) {
2854
3604
  sort: "timestamp",
2855
3605
  sort_dir: "desc"
2856
3606
  });
2857
- const messages = isRecord10(resp) ? resp.messages : null;
2858
- const matches = isRecord10(messages) ? asArray4(messages.matches).filter(isRecord10) : [];
3607
+ const messages = isRecord5(resp) ? resp.messages : null;
3608
+ const matches = isRecord5(messages) ? asArray(messages.matches).filter(isRecord5) : [];
2859
3609
  out.push(...matches);
2860
- const paging = isRecord10(messages) ? messages.paging ?? messages.pagination : null;
2861
- const totalPages = Number(isRecord10(paging) ? paging.pages ?? 1 : 1);
3610
+ const paging = isRecord5(messages) ? messages.paging ?? messages.pagination : null;
3611
+ const totalPages = Number(isRecord5(paging) ? paging.pages ?? 1 : 1);
2862
3612
  if (Number.isFinite(totalPages) && totalPages > 0) {
2863
3613
  pages = totalPages;
2864
3614
  }
@@ -2889,11 +3639,11 @@ async function searchFilesRaw(client, input) {
2889
3639
  sort: "timestamp",
2890
3640
  sort_dir: "desc"
2891
3641
  });
2892
- const files = isRecord10(resp) ? resp.files : null;
2893
- const matches = isRecord10(files) ? asArray4(files.matches).filter(isRecord10) : [];
3642
+ const files = isRecord5(resp) ? resp.files : null;
3643
+ const matches = isRecord5(files) ? asArray(files.matches).filter(isRecord5) : [];
2894
3644
  out.push(...matches);
2895
- const paging = isRecord10(files) ? files.paging ?? files.pagination : null;
2896
- const totalPages = Number(isRecord10(paging) ? paging.pages ?? 1 : 1);
3645
+ const paging = isRecord5(files) ? files.paging ?? files.pagination : null;
3646
+ const totalPages = Number(isRecord5(paging) ? paging.pages ?? 1 : 1);
2897
3647
  if (Number.isFinite(totalPages) && totalPages > 0) {
2898
3648
  pages = totalPages;
2899
3649
  }
@@ -2912,7 +3662,7 @@ async function searchFilesRaw(client, input) {
2912
3662
  }
2913
3663
 
2914
3664
  // src/slack/search-file-ext.ts
2915
- function inferExt2(file) {
3665
+ function inferExt(file) {
2916
3666
  const mt = (file.mimetype || "").toLowerCase();
2917
3667
  const ft = (file.filetype || "").toLowerCase();
2918
3668
  if (mt === "image/png" || ft === "png") {
@@ -2950,22 +3700,22 @@ async function searchFilesViaSearchApi(client, input) {
2950
3700
  const downloadsDir = await ensureDownloadsDir();
2951
3701
  const out = [];
2952
3702
  for (const f of matches) {
2953
- const mode = getString5(f.mode);
2954
- const mimetype = getString5(f.mimetype);
3703
+ const mode = getString(f.mode);
3704
+ const mimetype = getString(f.mimetype);
2955
3705
  if (!passesFileContentTypeFilter({ mode, mimetype }, input.contentType)) {
2956
3706
  continue;
2957
3707
  }
2958
- const url = getString5(f.url_private_download) ?? getString5(f.url_private);
3708
+ const url = getString(f.url_private_download) ?? getString(f.url_private);
2959
3709
  if (!url) {
2960
3710
  continue;
2961
3711
  }
2962
- const ext = inferExt2({
3712
+ const ext = inferExt({
2963
3713
  mimetype,
2964
- filetype: getString5(f.filetype),
2965
- name: getString5(f.name),
2966
- title: getString5(f.title)
3714
+ filetype: getString(f.filetype),
3715
+ name: getString(f.name),
3716
+ title: getString(f.title)
2967
3717
  });
2968
- const id = getString5(f.id);
3718
+ const id = getString(f.id);
2969
3719
  if (!id) {
2970
3720
  continue;
2971
3721
  }
@@ -2975,7 +3725,7 @@ async function searchFilesViaSearchApi(client, input) {
2975
3725
  destDir: downloadsDir,
2976
3726
  preferredName: `${id}${ext ? `.${ext}` : ""}`
2977
3727
  });
2978
- const title = (getString5(f.title) || getString5(f.name) || "").trim();
3728
+ const title = (getString(f.title) || getString(f.name) || "").trim();
2979
3729
  out.push({
2980
3730
  title: title || undefined,
2981
3731
  mimetype,
@@ -3007,31 +3757,31 @@ async function searchFilesInChannelsFallback(client, input) {
3007
3757
  count: 100,
3008
3758
  page
3009
3759
  });
3010
- const files = isRecord10(resp) ? asArray4(resp.files).filter(isRecord10) : [];
3760
+ const files = isRecord5(resp) ? asArray(resp.files).filter(isRecord5) : [];
3011
3761
  if (files.length === 0) {
3012
3762
  break;
3013
3763
  }
3014
3764
  for (const f of files) {
3015
- const mode = getString5(f.mode);
3016
- const mimetype = getString5(f.mimetype);
3765
+ const mode = getString(f.mode);
3766
+ const mimetype = getString(f.mimetype);
3017
3767
  if (!passesFileContentTypeFilter({ mode, mimetype }, input.contentType)) {
3018
3768
  continue;
3019
3769
  }
3020
- const title = (getString5(f.title) || getString5(f.name) || "").trim();
3770
+ const title = (getString(f.title) || getString(f.name) || "").trim();
3021
3771
  if (queryLower && !title.toLowerCase().includes(queryLower)) {
3022
3772
  continue;
3023
3773
  }
3024
- const url = getString5(f.url_private_download) ?? getString5(f.url_private);
3774
+ const url = getString(f.url_private_download) ?? getString(f.url_private);
3025
3775
  if (!url) {
3026
3776
  continue;
3027
3777
  }
3028
- const ext = inferExt2({
3778
+ const ext = inferExt({
3029
3779
  mimetype,
3030
- filetype: getString5(f.filetype),
3031
- name: getString5(f.name),
3032
- title: getString5(f.title)
3780
+ filetype: getString(f.filetype),
3781
+ name: getString(f.name),
3782
+ title: getString(f.title)
3033
3783
  });
3034
- const id = getString5(f.id);
3784
+ const id = getString(f.id);
3035
3785
  if (!id) {
3036
3786
  continue;
3037
3787
  }
@@ -3051,8 +3801,8 @@ async function searchFilesInChannelsFallback(client, input) {
3051
3801
  return out;
3052
3802
  }
3053
3803
  }
3054
- const paging = isRecord10(resp) ? isRecord10(resp.paging) ? resp.paging : resp.pagination : null;
3055
- const pages = Number(isRecord10(paging) ? paging.pages ?? paging.page_count : undefined);
3804
+ const paging = isRecord5(resp) ? isRecord5(resp.paging) ? resp.paging : resp.pagination : null;
3805
+ const pages = Number(isRecord5(paging) ? paging.pages ?? paging.page_count : undefined);
3056
3806
  if (Number.isFinite(pages) && page >= pages) {
3057
3807
  break;
3058
3808
  }
@@ -3088,19 +3838,19 @@ async function searchMessagesViaSearchApi(client, input) {
3088
3838
  }
3089
3839
  const messageRefs = [];
3090
3840
  for (const m of matches) {
3091
- const ts = getString5(m.ts)?.trim() ?? "";
3841
+ const ts = getString(m.ts)?.trim() ?? "";
3092
3842
  if (!ts) {
3093
3843
  continue;
3094
3844
  }
3095
- const channelValue = isRecord10(m.channel) ? m.channel : null;
3096
- const channelId = channelValue && getString5(channelValue.id) ? getString5(channelValue.id) : channelValue && getString5(channelValue.name) ? await resolveChannelId(client, `#${getString5(channelValue.name)}`) : "";
3845
+ const channelValue = isRecord5(m.channel) ? m.channel : null;
3846
+ const channelId = channelValue && getString(channelValue.id) ? getString(channelValue.id) : channelValue && getString(channelValue.name) ? await resolveChannelId(client, `#${getString(channelValue.name)}`) : "";
3097
3847
  if (!channelId) {
3098
3848
  continue;
3099
3849
  }
3100
3850
  messageRefs.push({
3101
3851
  channel_id: channelId,
3102
3852
  message_ts: ts,
3103
- permalink: getString5(m.permalink)
3853
+ permalink: getString(m.permalink)
3104
3854
  });
3105
3855
  if (messageRefs.length >= input.limit) {
3106
3856
  break;
@@ -3170,7 +3920,7 @@ async function searchMessagesInChannelsFallback(client, input) {
3170
3920
  limit: 200,
3171
3921
  latest: cursorLatest
3172
3922
  });
3173
- const messages = isRecord10(resp) ? asArray4(resp.messages).filter(isRecord10) : [];
3923
+ const messages = isRecord5(resp) ? asArray(resp.messages).filter(isRecord5) : [];
3174
3924
  if (messages.length === 0) {
3175
3925
  break;
3176
3926
  }
@@ -3217,7 +3967,7 @@ async function searchMessagesInChannelsFallback(client, input) {
3217
3967
  break;
3218
3968
  }
3219
3969
  const last = messages.at(-1);
3220
- cursorLatest = last ? getString5(last.ts) : undefined;
3970
+ cursorLatest = last ? getString(last.ts) : undefined;
3221
3971
  if (!cursorLatest) {
3222
3972
  break;
3223
3973
  }
@@ -3260,7 +4010,7 @@ async function downloadFilesForMessage(input) {
3260
4010
  if (!url) {
3261
4011
  continue;
3262
4012
  }
3263
- const ext = inferExt2(f);
4013
+ const ext = inferExt(f);
3264
4014
  const path = await downloadSlackFile({
3265
4015
  auth: input.auth,
3266
4016
  url,
@@ -3271,15 +4021,15 @@ async function downloadFilesForMessage(input) {
3271
4021
  }
3272
4022
  }
3273
4023
  function messageSummaryFromApiMessage(channelId, msg) {
3274
- const text = getString5(msg.text) ?? "";
3275
- const files = asArray4(msg.files).map((f) => toSlackFileSummary2(f)).filter((f) => f !== null);
4024
+ const text = getString(msg.text) ?? "";
4025
+ const files = asArray(msg.files).map((f) => toSlackFileSummary2(f)).filter((f) => f !== null);
3276
4026
  return {
3277
4027
  channel_id: channelId,
3278
- ts: getString5(msg.ts) ?? "",
3279
- thread_ts: getString5(msg.thread_ts),
3280
- reply_count: getNumber3(msg.reply_count),
3281
- user: getString5(msg.user),
3282
- bot_id: getString5(msg.bot_id),
4028
+ ts: getString(msg.ts) ?? "",
4029
+ thread_ts: getString(msg.thread_ts),
4030
+ reply_count: getNumber(msg.reply_count),
4031
+ user: getString(msg.user),
4032
+ bot_id: getString(msg.bot_id),
3283
4033
  text,
3284
4034
  markdown: slackMrkdwnToMarkdown(text),
3285
4035
  blocks: Array.isArray(msg.blocks) ? msg.blocks : undefined,
@@ -3289,24 +4039,24 @@ function messageSummaryFromApiMessage(channelId, msg) {
3289
4039
  };
3290
4040
  }
3291
4041
  function toSlackFileSummary2(value) {
3292
- if (!isRecord10(value)) {
4042
+ if (!isRecord5(value)) {
3293
4043
  return null;
3294
4044
  }
3295
- const id = getString5(value.id);
4045
+ const id = getString(value.id);
3296
4046
  if (!id) {
3297
4047
  return null;
3298
4048
  }
3299
4049
  return {
3300
4050
  id,
3301
- name: getString5(value.name),
3302
- title: getString5(value.title),
3303
- mimetype: getString5(value.mimetype),
3304
- filetype: getString5(value.filetype),
3305
- mode: getString5(value.mode),
3306
- permalink: getString5(value.permalink),
3307
- url_private: getString5(value.url_private),
3308
- url_private_download: getString5(value.url_private_download),
3309
- size: getNumber3(value.size)
4051
+ name: getString(value.name),
4052
+ title: getString(value.title),
4053
+ mimetype: getString(value.mimetype),
4054
+ filetype: getString(value.filetype),
4055
+ mode: getString(value.mode),
4056
+ permalink: getString(value.permalink),
4057
+ url_private: getString(value.url_private),
4058
+ url_private_download: getString(value.url_private_download),
4059
+ size: getNumber(value.size)
3310
4060
  };
3311
4061
  }
3312
4062
 
@@ -3383,7 +4133,7 @@ async function searchSlack(input) {
3383
4133
 
3384
4134
  // src/cli/search-command.ts
3385
4135
  function addSearchOptions(cmd) {
3386
- return cmd.option("--workspace <url>", "Workspace URL (needed when searching across multiple workspaces)").option("--channel <channel...>", "Channel filter (#name, name, or id). Repeatable.").option("--user <user>", "User filter (@name, name, or user id U...)").option("--after <date>", "Only results after YYYY-MM-DD").option("--before <date>", "Only results before YYYY-MM-DD").option("--content-type <type>", "Filter content type: any|text|image|snippet|file (default any)").option("--limit <n>", "Max results (default 20)", "20").option("--max-content-chars <n>", "Max message content characters (default 4000, -1 for unlimited)", "4000");
4136
+ return cmd.option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when searching across multiple workspaces)").option("--channel <channel...>", "Channel filter (#name, name, or id). Repeatable.").option("--user <user>", "User filter (@name, name, or user id U...)").option("--after <date>", "Only results after YYYY-MM-DD").option("--before <date>", "Only results before YYYY-MM-DD").option("--content-type <type>", "Filter content type: any|text|image|snippet|file (default any)").option("--limit <n>", "Max results (default 20)", "20").option("--max-content-chars <n>", "Max message content characters (default 4000, -1 for unlimited)", "4000");
3387
4137
  }
3388
4138
  async function runSearch(input) {
3389
4139
  const workspaceUrl = input.ctx.effectiveWorkspaceUrl(input.options.workspace);
@@ -3435,13 +4185,13 @@ function registerSearchCommand(input) {
3435
4185
 
3436
4186
  // src/lib/update.ts
3437
4187
  import { createHash } from "node:crypto";
3438
- import { chmod, copyFile, mkdir as mkdir5, readFile as readFile4, rename, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
4188
+ import { chmod, copyFile, mkdir as mkdir5, readFile as readFile5, rename, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
3439
4189
  import { tmpdir as tmpdir2 } from "node:os";
3440
- import { join as join8 } from "node:path";
4190
+ import { join as join10 } from "node:path";
3441
4191
  var REPO = "stablyai/agent-slack";
3442
4192
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
3443
4193
  function getCachePath() {
3444
- return join8(getAppDir(), "update-check.json");
4194
+ return join10(getAppDir(), "update-check.json");
3445
4195
  }
3446
4196
  function compareSemver(a, b) {
3447
4197
  const pa = a.replace(/^v/, "").split(".").map(Number);
@@ -3498,24 +4248,24 @@ async function checkForUpdate(force = false) {
3498
4248
  };
3499
4249
  }
3500
4250
  function detectPlatformAsset() {
3501
- const platform5 = process.platform === "win32" ? "windows" : process.platform;
4251
+ const platform6 = process.platform === "win32" ? "windows" : process.platform;
3502
4252
  const archMap = { x64: "x64", arm64: "arm64" };
3503
4253
  const arch = archMap[process.arch] ?? process.arch;
3504
- const ext = platform5 === "windows" ? ".exe" : "";
3505
- return `agent-slack-${platform5}-${arch}${ext}`;
4254
+ const ext = platform6 === "windows" ? ".exe" : "";
4255
+ return `agent-slack-${platform6}-${arch}${ext}`;
3506
4256
  }
3507
4257
  async function sha256(filePath) {
3508
- const data = await readFile4(filePath);
4258
+ const data = await readFile5(filePath);
3509
4259
  return createHash("sha256").update(data).digest("hex");
3510
4260
  }
3511
4261
  async function performUpdate(latest) {
3512
4262
  const asset = detectPlatformAsset();
3513
4263
  const tag = `v${latest}`;
3514
4264
  const baseUrl = `https://github.com/${REPO}/releases/download/${tag}`;
3515
- const tmp = join8(tmpdir2(), `agent-slack-update-${Date.now()}`);
4265
+ const tmp = join10(tmpdir2(), `agent-slack-update-${Date.now()}`);
3516
4266
  await mkdir5(tmp, { recursive: true });
3517
- const binTmp = join8(tmp, asset);
3518
- const sumsTmp = join8(tmp, "checksums-sha256.txt");
4267
+ const binTmp = join10(tmp, asset);
4268
+ const sumsTmp = join10(tmp, "checksums-sha256.txt");
3519
4269
  try {
3520
4270
  const [binResp, sumsResp] = await Promise.all([
3521
4271
  fetch(`${baseUrl}/${asset}`, { signal: AbortSignal.timeout(120000) }),
@@ -3527,9 +4277,9 @@ async function performUpdate(latest) {
3527
4277
  if (!sumsResp.ok) {
3528
4278
  return { success: false, message: `Failed to download checksums: HTTP ${sumsResp.status}` };
3529
4279
  }
3530
- await writeFile3(binTmp, Buffer.from(await binResp.arrayBuffer()));
4280
+ await writeFile4(binTmp, Buffer.from(await binResp.arrayBuffer()));
3531
4281
  const sumsText = await sumsResp.text();
3532
- await writeFile3(sumsTmp, sumsText);
4282
+ await writeFile4(sumsTmp, sumsText);
3533
4283
  const expected = sumsText.split(`
3534
4284
  `).map((line) => line.trim().split(/\s+/)).find((parts) => parts[1] === asset)?.[0];
3535
4285
  if (!expected) {
@@ -3623,9 +4373,9 @@ async function listUsers(client, options) {
3623
4373
  limit: pageSize,
3624
4374
  cursor
3625
4375
  });
3626
- const members = asArray5(resp.members).filter(isRecord11);
4376
+ const members = asArray(resp.members).filter(isRecord5);
3627
4377
  for (const m of members) {
3628
- const id = getString6(m.id);
4378
+ const id = getString(m.id);
3629
4379
  if (!id) {
3630
4380
  continue;
3631
4381
  }
@@ -3637,8 +4387,8 @@ async function listUsers(client, options) {
3637
4387
  break;
3638
4388
  }
3639
4389
  }
3640
- const meta = isRecord11(resp.response_metadata) ? resp.response_metadata : null;
3641
- const next = meta ? getString6(meta.next_cursor) : undefined;
4390
+ const meta = isRecord5(resp.response_metadata) ? resp.response_metadata : null;
4391
+ const next = meta ? getString(meta.next_cursor) : undefined;
3642
4392
  if (!next) {
3643
4393
  return { users: out };
3644
4394
  }
@@ -3656,8 +4406,8 @@ async function getUser(client, input) {
3656
4406
  throw new Error(`Could not resolve user: ${input}`);
3657
4407
  }
3658
4408
  const resp = await client.api("users.info", { user: userId });
3659
- const u = isRecord11(resp.user) ? resp.user : null;
3660
- if (!u || !getString6(u.id)) {
4409
+ const u = isRecord5(resp.user) ? resp.user : null;
4410
+ if (!u || !getString(u.id)) {
3661
4411
  throw new Error("users.info returned no user");
3662
4412
  }
3663
4413
  return toCompactUser(u);
@@ -3674,16 +4424,16 @@ async function resolveUserId2(client, input) {
3674
4424
  let cursor;
3675
4425
  for (;; ) {
3676
4426
  const resp = await client.api("users.list", { limit: 200, cursor });
3677
- const members = asArray5(resp.members).filter(isRecord11);
3678
- const found = members.find((m) => getString6(m.name) === handle);
4427
+ const members = asArray(resp.members).filter(isRecord5);
4428
+ const found = members.find((m) => getString(m.name) === handle);
3679
4429
  if (found) {
3680
- const id = getString6(found.id);
4430
+ const id = getString(found.id);
3681
4431
  if (id) {
3682
4432
  return id;
3683
4433
  }
3684
4434
  }
3685
- const meta = isRecord11(resp.response_metadata) ? resp.response_metadata : null;
3686
- const next = meta ? getString6(meta.next_cursor) : undefined;
4435
+ const meta = isRecord5(resp.response_metadata) ? resp.response_metadata : null;
4436
+ const next = meta ? getString(meta.next_cursor) : undefined;
3687
4437
  if (!next) {
3688
4438
  break;
3689
4439
  }
@@ -3692,33 +4442,24 @@ async function resolveUserId2(client, input) {
3692
4442
  return null;
3693
4443
  }
3694
4444
  function toCompactUser(u) {
3695
- const profile = isRecord11(u.profile) ? u.profile : {};
4445
+ const profile = isRecord5(u.profile) ? u.profile : {};
3696
4446
  return {
3697
- id: getString6(u.id) ?? "",
3698
- name: getString6(u.name) ?? undefined,
3699
- real_name: getString6(u.real_name) ?? undefined,
3700
- display_name: getString6(profile.display_name) ?? undefined,
3701
- email: getString6(profile.email) ?? undefined,
3702
- title: getString6(profile.title) ?? undefined,
3703
- tz: getString6(u.tz) ?? undefined,
4447
+ id: getString(u.id) ?? "",
4448
+ name: getString(u.name) ?? undefined,
4449
+ real_name: getString(u.real_name) ?? undefined,
4450
+ display_name: getString(profile.display_name) ?? undefined,
4451
+ email: getString(profile.email) ?? undefined,
4452
+ title: getString(profile.title) ?? undefined,
4453
+ tz: getString(u.tz) ?? undefined,
3704
4454
  is_bot: typeof u.is_bot === "boolean" ? u.is_bot : undefined,
3705
4455
  deleted: typeof u.deleted === "boolean" ? u.deleted : undefined
3706
4456
  };
3707
4457
  }
3708
- function isRecord11(value) {
3709
- return typeof value === "object" && value !== null;
3710
- }
3711
- function asArray5(value) {
3712
- return Array.isArray(value) ? value : [];
3713
- }
3714
- function getString6(value) {
3715
- return typeof value === "string" ? value : undefined;
3716
- }
3717
4458
 
3718
4459
  // src/cli/user-command.ts
3719
4460
  function registerUserCommand(input) {
3720
4461
  const userCmd = input.program.command("user").description("Workspace user directory");
3721
- userCmd.command("list").description("List users in the workspace").option("--workspace <url>", "Workspace URL (required if you have multiple workspaces)").option("--limit <n>", "Max users (default 200)", "200").option("--cursor <cursor>", "Pagination cursor").option("--include-bots", "Include bot users").action(async (...args) => {
4462
+ userCmd.command("list").description("List users in the workspace").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if you have multiple workspaces)").option("--limit <n>", "Max users (default 200)", "200").option("--cursor <cursor>", "Pagination cursor").option("--include-bots", "Include bot users").action(async (...args) => {
3722
4463
  const [options] = args;
3723
4464
  try {
3724
4465
  const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
@@ -3740,7 +4481,7 @@ function registerUserCommand(input) {
3740
4481
  process.exitCode = 1;
3741
4482
  }
3742
4483
  });
3743
- userCmd.command("get").description("Get a single user by id (U...) or handle (@name)").argument("<user>", "User id (U...) or @handle/handle").option("--workspace <url>", "Workspace URL (required if you have multiple workspaces)").action(async (...args) => {
4484
+ userCmd.command("get").description("Get a single user by id (U...) or handle (@name)").argument("<user>", "User id (U...) or @handle/handle").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if you have multiple workspaces)").action(async (...args) => {
3744
4485
  const [user, options] = args;
3745
4486
  try {
3746
4487
  const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
@@ -3778,5 +4519,5 @@ if (subcommand && subcommand !== "update") {
3778
4519
  backgroundUpdateCheck();
3779
4520
  }
3780
4521
 
3781
- //# debugId=FD1EE5153F86A96C64756E2164756E21
4522
+ //# debugId=6879354D2CF2337D64756E2164756E21
3782
4523
  //# sourceMappingURL=index.js.map