agent-slack 0.2.12 → 0.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@ Guiding principle:
9
9
 
10
10
  ## At a glance
11
11
 
12
- - **Read**: fetch a message, detect threads, list full threads
12
+ - **Read**: fetch a message, browse channel history, list full threads
13
13
  - **Search**: messages + files (with filters)
14
14
  - **Artifacts**: auto-download snippets/images/files to local paths for agents
15
15
  - **Write**: reply in thread, add reactions
@@ -17,10 +17,18 @@ Guiding principle:
17
17
 
18
18
  ## Installation
19
19
 
20
+ Recommended (Bun install script):
21
+
20
22
  ```bash
21
23
  curl -fsSL https://raw.githubusercontent.com/stablyai/agent-slack/master/install.sh | sh
22
24
  ```
23
25
 
26
+ OR npm global install (requires Node >= 22.5):
27
+
28
+ ```bash
29
+ npm i -g agent-slack
30
+ ```
31
+
24
32
  ## Agent skill
25
33
 
26
34
  This repo ships an agent skill at `skills/agent-slack/` compatible with Claude Code, Codex, Cursor, etc
@@ -50,7 +58,7 @@ agent-slack
50
58
  │ └── parse-curl
51
59
  ├── message
52
60
  │ ├── get <target> # fetch 1 message (+ thread meta )
53
- │ ├── list <target> # fetch full thread
61
+ │ ├── list <target> # fetch thread or recent channel messages
54
62
  │ ├── send <target> <text> # send / reply (does the right thing)
55
63
  │ └── react
56
64
  │ ├── add <target> <emoji>
@@ -138,6 +146,9 @@ agent-slack message get "https://workspace.slack.com/archives/C123/p170000000000
138
146
 
139
147
  # Full thread for a message
140
148
  agent-slack message list "https://workspace.slack.com/archives/C123/p1700000000000000"
149
+
150
+ # Recent channel messages (browse channel history)
151
+ agent-slack message list "#general" --limit 20
141
152
  ```
142
153
 
143
154
  Optional:
@@ -158,7 +169,7 @@ agent-slack message get "https://workspace.slack.com/archives/C123/p170000000000
158
169
  }
159
170
  ```
160
171
 
161
- **`message list`** fetches all messages in a thread (or channel history if no thread). Use this when you need the full conversation:
172
+ **`message list`** fetches all replies in a thread, or recent channel messages when no thread is specified. Use this when you need the full conversation:
162
173
 
163
174
  ```json
164
175
  {
@@ -173,6 +184,7 @@ When to use which:
173
184
 
174
185
  - Use `get` to check a single message or see if there's a thread worth expanding
175
186
  - Use `list` to read an entire thread conversation
187
+ - Use `list` on a channel (without `--thread-ts`) to browse recent channel messages
176
188
 
177
189
  ### Files (snippets/images/attachments)
178
190
 
package/dist/index.js CHANGED
@@ -176,9 +176,9 @@ function parseSlackCurlCommand(curlInput) {
176
176
  // src/auth/desktop.ts
177
177
  import { cp, mkdir, rm, unlink } from "node:fs/promises";
178
178
  import { existsSync as existsSync2 } from "node:fs";
179
- import { execFileSync, execSync as execSync2 } from "node:child_process";
179
+ import { execFileSync } from "node:child_process";
180
180
  import { pbkdf2Sync, createDecipheriv } from "node:crypto";
181
- import { homedir } from "node:os";
181
+ import { homedir, platform as platform2 } from "node:os";
182
182
  import { join as join3 } from "node:path";
183
183
 
184
184
  // src/lib/leveldb-reader.ts
@@ -441,6 +441,21 @@ async function findKeysContaining(dir, substring) {
441
441
  }
442
442
 
443
443
  // src/auth/desktop.ts
444
+ function isMissingBunSqliteModule(error) {
445
+ if (!error || typeof error !== "object") {
446
+ return false;
447
+ }
448
+ const err = error;
449
+ const code = typeof err.code === "string" ? err.code : "";
450
+ const message = typeof err.message === "string" ? err.message : "";
451
+ if (code === "ERR_MODULE_NOT_FOUND" || code === "ERR_UNSUPPORTED_ESM_URL_SCHEME") {
452
+ return true;
453
+ }
454
+ if (!message.includes("bun:sqlite")) {
455
+ return false;
456
+ }
457
+ 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
+ }
444
459
  async function queryReadonlySqlite(dbPath, sql) {
445
460
  try {
446
461
  const { Database } = await import("bun:sqlite");
@@ -450,7 +465,10 @@ async function queryReadonlySqlite(dbPath, sql) {
450
465
  } finally {
451
466
  db.close();
452
467
  }
453
- } catch {
468
+ } catch (error) {
469
+ if (!isMissingBunSqliteModule(error)) {
470
+ throw error;
471
+ }
454
472
  const { DatabaseSync } = await import("node:sqlite");
455
473
  const db = new DatabaseSync(dbPath, { readOnly: true });
456
474
  try {
@@ -460,14 +478,24 @@ async function queryReadonlySqlite(dbPath, sql) {
460
478
  }
461
479
  }
462
480
  }
481
+ var PLATFORM = platform2();
482
+ var IS_MACOS2 = PLATFORM === "darwin";
483
+ var IS_LINUX = PLATFORM === "linux";
463
484
  var SLACK_SUPPORT_DIR_ELECTRON = join3(homedir(), "Library", "Application Support", "Slack");
464
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");
465
488
  function getSlackPaths() {
466
- const candidates = [SLACK_SUPPORT_DIR_ELECTRON, SLACK_SUPPORT_DIR_APPSTORE];
489
+ const candidates = IS_MACOS2 ? [SLACK_SUPPORT_DIR_ELECTRON, SLACK_SUPPORT_DIR_APPSTORE] : IS_LINUX ? [SLACK_SUPPORT_DIR_LINUX_FLATPAK, SLACK_SUPPORT_DIR_LINUX] : [];
490
+ if (candidates.length === 0) {
491
+ throw new Error(`Slack Desktop extraction is not supported on ${PLATFORM}.`);
492
+ }
467
493
  for (const dir of candidates) {
468
494
  const leveldbDir = join3(dir, "Local Storage", "leveldb");
469
495
  if (existsSync2(leveldbDir)) {
470
- return { leveldbDir, cookiesDb: join3(dir, "Cookies") };
496
+ const cookiesDbCandidates = [join3(dir, "Network", "Cookies"), join3(dir, "Cookies")];
497
+ const cookiesDb = cookiesDbCandidates.find((candidate) => existsSync2(candidate)) || cookiesDbCandidates[0];
498
+ return { leveldbDir, cookiesDb };
471
499
  }
472
500
  }
473
501
  throw new Error(`Slack Desktop data not found. Checked:
@@ -493,11 +521,17 @@ async function snapshotLevelDb(srcDir) {
493
521
  const base = join3(homedir(), ".config", "agent-slack", "cache", "leveldb-snapshots");
494
522
  const dest = join3(base, `${Date.now()}`);
495
523
  await mkdir(base, { recursive: true });
496
- try {
497
- execFileSync("cp", ["-cR", srcDir, dest], {
498
- stdio: ["ignore", "ignore", "ignore"]
499
- });
500
- } catch {
524
+ let useNodeCopy = !IS_MACOS2;
525
+ if (IS_MACOS2) {
526
+ try {
527
+ execFileSync("cp", ["-cR", srcDir, dest], {
528
+ stdio: ["ignore", "ignore", "ignore"]
529
+ });
530
+ } catch {
531
+ useNodeCopy = true;
532
+ }
533
+ }
534
+ if (useNodeCopy) {
501
535
  await cp(srcDir, dest, { recursive: true, force: true });
502
536
  }
503
537
  try {
@@ -552,11 +586,18 @@ async function extractTeamsFromSlackLevelDb(leveldbDir) {
552
586
  const localConfigV3 = Buffer.from("localConfig_v3");
553
587
  const entries = await findKeysContaining(snap, Buffer.from("localConfig_v"));
554
588
  let configBuf = null;
589
+ let configRank = -1n;
555
590
  for (const entry of entries) {
556
591
  if (entry.key.includes(localConfigV2) || entry.key.includes(localConfigV3)) {
557
592
  if (entry.value && entry.value.length > 0) {
558
- configBuf = entry.value;
559
- break;
593
+ let rank = 0n;
594
+ if (entry.key.length >= 8) {
595
+ rank = entry.key.readBigUInt64LE(entry.key.length - 8);
596
+ }
597
+ if (!configBuf || rank >= configRank) {
598
+ configBuf = entry.value;
599
+ configRank = rank;
600
+ }
560
601
  }
561
602
  }
562
603
  }
@@ -577,30 +618,59 @@ async function extractTeamsFromSlackLevelDb(leveldbDir) {
577
618
  } catch {}
578
619
  }
579
620
  }
580
- function getSafeStoragePassword() {
581
- const services = ["Slack Safe Storage", "Chrome Safe Storage", "Chromium Safe Storage"];
582
- for (const svc of services) {
583
- try {
584
- const out = execSync2(`security find-generic-password -w -s ${JSON.stringify(svc)} 2>/dev/null`, {
585
- encoding: "utf8",
586
- stdio: ["ignore", "pipe", "ignore"]
587
- }).trim();
588
- if (out) {
589
- return out;
590
- }
591
- } catch {}
621
+ function getSafeStoragePasswords(prefix) {
622
+ if (IS_MACOS2) {
623
+ const services = ["Slack Safe Storage", "Chrome Safe Storage", "Chromium Safe Storage"];
624
+ const passwords = [];
625
+ for (const service of services) {
626
+ try {
627
+ const out = execFileSync("security", ["find-generic-password", "-w", "-s", service], {
628
+ encoding: "utf8",
629
+ stdio: ["ignore", "pipe", "ignore"]
630
+ }).trim();
631
+ if (out) {
632
+ passwords.push(out);
633
+ }
634
+ } catch {}
635
+ }
636
+ if (passwords.length > 0) {
637
+ return passwords;
638
+ }
639
+ }
640
+ if (IS_LINUX) {
641
+ const attributes = [
642
+ ["application", "com.slack.Slack"],
643
+ ["application", "Slack"],
644
+ ["application", "slack"],
645
+ ["service", "Slack Safe Storage"]
646
+ ];
647
+ const passwords = [];
648
+ for (const pair of attributes) {
649
+ try {
650
+ const out = execFileSync("secret-tool", ["lookup", ...pair], {
651
+ encoding: "utf8",
652
+ stdio: ["ignore", "pipe", "ignore"]
653
+ }).trim();
654
+ if (out) {
655
+ passwords.push(out);
656
+ }
657
+ } catch {}
658
+ }
659
+ if (prefix === "v11") {
660
+ passwords.push("");
661
+ }
662
+ passwords.push("peanuts");
663
+ return [...new Set(passwords)];
592
664
  }
593
- throw new Error('Could not read Safe Storage password from Keychain (tried "Slack Safe Storage").');
665
+ throw new Error("Could not read Safe Storage password from desktop keychain.");
594
666
  }
595
- function decryptChromiumCookieValue(encrypted, password) {
596
- if (!encrypted || encrypted.length === 0) {
667
+ function decryptChromiumCookieValue(data, password) {
668
+ if (!data || data.length === 0) {
597
669
  return "";
598
670
  }
599
- const prefix = encrypted.subarray(0, 3).toString("utf8");
600
- const data = prefix === "v10" || prefix === "v11" ? encrypted.subarray(3) : encrypted;
601
671
  const salt = Buffer.from("saltysalt", "utf8");
602
672
  const iv = Buffer.alloc(16, " ");
603
- const key = pbkdf2Sync(password, salt, 1003, 16, "sha1");
673
+ const key = pbkdf2Sync(password, salt, IS_LINUX ? 1 : 1003, 16, "sha1");
604
674
  const decipher = createDecipheriv("aes-128-cbc", key, iv);
605
675
  decipher.setAutoPadding(true);
606
676
  const plain = Buffer.concat([decipher.update(data), decipher.final()]);
@@ -640,13 +710,19 @@ async function extractCookieDFromSlackCookiesDb(cookiesPath) {
640
710
  if (encrypted.length === 0) {
641
711
  throw new Error("Slack 'd' cookie had no encrypted_value");
642
712
  }
643
- const password = getSafeStoragePassword();
644
- const decrypted = decryptChromiumCookieValue(encrypted, password);
645
- const match = decrypted.match(/xoxd-[A-Za-z0-9%/+_=.-]+/);
646
- if (!match) {
647
- throw new Error("Could not locate xoxd-* in decrypted Slack cookie");
713
+ const prefix = encrypted.subarray(0, 3).toString("utf8");
714
+ const data = prefix === "v10" || prefix === "v11" ? encrypted.subarray(3) : encrypted;
715
+ const passwords = getSafeStoragePasswords(prefix);
716
+ for (const password of passwords) {
717
+ try {
718
+ const decrypted = decryptChromiumCookieValue(data, password);
719
+ const match = decrypted.match(/xoxd-[A-Za-z0-9%/+_=.-]+/);
720
+ if (match) {
721
+ return match[0];
722
+ }
723
+ } catch {}
648
724
  }
649
- return match[0];
725
+ throw new Error("Could not locate xoxd-* in decrypted Slack cookie");
650
726
  }
651
727
  async function extractFromSlackDesktop() {
652
728
  const { leveldbDir, cookiesDb } = getSlackPaths();
@@ -720,11 +796,11 @@ var CredentialsSchema = z.object({
720
796
  });
721
797
 
722
798
  // src/auth/keychain.ts
723
- import { platform as platform2 } from "node:os";
799
+ import { platform as platform3 } from "node:os";
724
800
  import { execFileSync as execFileSync2 } from "node:child_process";
725
- var IS_MACOS2 = platform2() === "darwin";
801
+ var IS_MACOS3 = platform3() === "darwin";
726
802
  function keychainGet(account, service) {
727
- if (!IS_MACOS2) {
803
+ if (!IS_MACOS3) {
728
804
  return null;
729
805
  }
730
806
  try {
@@ -735,7 +811,7 @@ function keychainGet(account, service) {
735
811
  }
736
812
  }
737
813
  function keychainSet(input) {
738
- if (!IS_MACOS2) {
814
+ if (!IS_MACOS3) {
739
815
  return false;
740
816
  }
741
817
  const { account, value, service } = input;
@@ -755,9 +831,9 @@ function keychainSet(input) {
755
831
  }
756
832
 
757
833
  // src/auth/store.ts
758
- import { platform as platform3 } from "node:os";
834
+ import { platform as platform4 } from "node:os";
759
835
  var KEYCHAIN_PLACEHOLDER = "__KEYCHAIN__";
760
- var IS_MACOS3 = platform3() === "darwin";
836
+ var IS_MACOS4 = platform4() === "darwin";
761
837
  function normalizeWorkspaceUrl(workspaceUrl) {
762
838
  const u = new URL(workspaceUrl);
763
839
  return `${u.protocol}//${u.host}`;
@@ -811,7 +887,7 @@ async function saveCredentials(credentials) {
811
887
  }))
812
888
  };
813
889
  const filePayload = structuredClone(payload);
814
- if (IS_MACOS3) {
890
+ if (IS_MACOS4) {
815
891
  const firstBrowser = payload.workspaces.find((w) => w.auth.auth_type === "browser");
816
892
  let xoxdStored = false;
817
893
  if (firstBrowser?.auth.auth_type === "browser" && !isPlaceholderSecret(firstBrowser.auth.xoxd_cookie)) {
@@ -941,8 +1017,24 @@ async function resolveChannelId(client, input) {
941
1017
  if (!name) {
942
1018
  throw new Error("Channel name is empty");
943
1019
  }
1020
+ try {
1021
+ const searchResp = await client.api("search.messages", {
1022
+ query: `in:#${name}`,
1023
+ count: 1,
1024
+ sort: "timestamp",
1025
+ sort_dir: "desc"
1026
+ });
1027
+ const messages = isRecord4(searchResp) ? searchResp.messages : null;
1028
+ const matches = isRecord4(messages) ? asArray(messages.matches).filter(isRecord4) : [];
1029
+ if (matches.length > 0) {
1030
+ const channel = isRecord4(matches[0].channel) ? matches[0].channel : null;
1031
+ const channelId = channel ? getString(channel.id) : undefined;
1032
+ if (channelId) {
1033
+ return channelId;
1034
+ }
1035
+ }
1036
+ } catch {}
944
1037
  let cursor;
945
- const matches = [];
946
1038
  for (;; ) {
947
1039
  const resp = await client.api("conversations.list", {
948
1040
  exclude_archived: true,
@@ -953,11 +1045,7 @@ async function resolveChannelId(client, input) {
953
1045
  const chans = asArray(resp.channels).filter(isRecord4);
954
1046
  for (const c of chans) {
955
1047
  if (getString(c.name) === name && getString(c.id)) {
956
- matches.push({
957
- id: getString(c.id) ?? "",
958
- name: getString(c.name) ?? undefined,
959
- is_private: typeof c.is_private === "boolean" ? c.is_private : undefined
960
- });
1048
+ return getString(c.id);
961
1049
  }
962
1050
  }
963
1051
  const meta = isRecord4(resp.response_metadata) ? resp.response_metadata : null;
@@ -967,13 +1055,7 @@ async function resolveChannelId(client, input) {
967
1055
  }
968
1056
  cursor = next;
969
1057
  }
970
- if (matches.length === 1) {
971
- return matches[0].id;
972
- }
973
- if (matches.length === 0) {
974
- throw new Error(`Could not resolve channel name: #${name}`);
975
- }
976
- throw new Error(`Ambiguous channel name: #${name} (matched ${matches.length} channels: ${matches.map((m) => m.id).join(", ")})`);
1058
+ throw new Error(`Could not resolve channel name: #${name}`);
977
1059
  }
978
1060
  function isRecord4(value) {
979
1061
  return typeof value === "object" && value !== null;
@@ -1099,7 +1181,7 @@ function normalizeUrl(u) {
1099
1181
  return `${url.protocol}//${url.host}`;
1100
1182
  }
1101
1183
  async function refreshFromDesktopIfPossible() {
1102
- if (process.platform !== "darwin") {
1184
+ if (process.platform !== "darwin" && process.platform !== "linux") {
1103
1185
  return false;
1104
1186
  }
1105
1187
  try {
@@ -2123,6 +2205,43 @@ async function findMessageInThread(client, input) {
2123
2205
  }
2124
2206
  return;
2125
2207
  }
2208
+ async function fetchChannelHistory(client, input) {
2209
+ const raw = input.limit ?? 25;
2210
+ const limit = Number.isFinite(raw) ? Math.min(Math.max(raw, 1), 200) : 25;
2211
+ const out = [];
2212
+ const resp = await client.api("conversations.history", {
2213
+ channel: input.channelId,
2214
+ limit,
2215
+ latest: input.latest,
2216
+ oldest: input.oldest,
2217
+ include_all_metadata: input.includeReactions ? true : undefined
2218
+ });
2219
+ const messages = asArray2(resp.messages);
2220
+ for (const m of messages) {
2221
+ if (!isRecord8(m)) {
2222
+ continue;
2223
+ }
2224
+ const files = asArray2(m.files).map((f) => toSlackFileSummary(f)).filter((f) => f !== null);
2225
+ const enrichedFiles = files.length > 0 ? await enrichFiles(client, files) : undefined;
2226
+ const text = getString4(m.text) ?? "";
2227
+ out.push({
2228
+ channel_id: input.channelId,
2229
+ ts: getString4(m.ts) ?? "",
2230
+ thread_ts: getString4(m.thread_ts),
2231
+ reply_count: getNumber(m.reply_count),
2232
+ user: getString4(m.user),
2233
+ bot_id: getString4(m.bot_id),
2234
+ text,
2235
+ markdown: slackMrkdwnToMarkdown(text),
2236
+ blocks: Array.isArray(m.blocks) ? m.blocks : undefined,
2237
+ attachments: Array.isArray(m.attachments) ? m.attachments : undefined,
2238
+ files: enrichedFiles,
2239
+ reactions: Array.isArray(m.reactions) ? m.reactions : undefined
2240
+ });
2241
+ }
2242
+ out.sort((a, b) => Number.parseFloat(a.ts) - Number.parseFloat(b.ts));
2243
+ return out;
2244
+ }
2126
2245
  async function fetchThread(client, input) {
2127
2246
  const out = [];
2128
2247
  let cursor;
@@ -2286,6 +2405,8 @@ function parseMsgTarget(input) {
2286
2405
  }
2287
2406
 
2288
2407
  // src/cli/message-actions.ts
2408
+ import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
2409
+ import { join as join8 } from "node:path";
2289
2410
  function isRecord9(value) {
2290
2411
  return typeof value === "object" && value !== null && !Array.isArray(value);
2291
2412
  }
@@ -2295,6 +2416,16 @@ function asArray3(value) {
2295
2416
  function getNumber2(value) {
2296
2417
  return typeof value === "number" ? value : undefined;
2297
2418
  }
2419
+ function parseLimit(raw) {
2420
+ if (raw === undefined) {
2421
+ return;
2422
+ }
2423
+ const n = Number.parseInt(raw, 10);
2424
+ if (!Number.isFinite(n) || n < 1) {
2425
+ throw new Error(`Invalid --limit value "${raw}": must be a positive integer`);
2426
+ }
2427
+ return n;
2428
+ }
2298
2429
  async function getThreadSummary(client, input) {
2299
2430
  const replyCount = input.msg.reply_count ?? 0;
2300
2431
  const rootTs = input.msg.thread_ts ?? (replyCount > 0 ? input.msg.ts : null);
@@ -2344,6 +2475,28 @@ function inferExt(file) {
2344
2475
  const m = name.match(/\.([A-Za-z0-9]{1,10})$/);
2345
2476
  return m ? m[1].toLowerCase() : null;
2346
2477
  }
2478
+ var CANVAS_MODES = new Set(["canvas", "quip", "docs"]);
2479
+ function looksLikeAuthPage(html) {
2480
+ return /<form[^>]+signin|data-qa="signin|<title>[^<]*Sign\s*in/i.test(html);
2481
+ }
2482
+ async function downloadCanvasAsMarkdown(input) {
2483
+ const htmlPath = await downloadSlackFile({
2484
+ auth: input.auth,
2485
+ url: input.url,
2486
+ destDir: input.destDir,
2487
+ preferredName: `${input.fileId}.html`,
2488
+ options: { allowHtml: true }
2489
+ });
2490
+ const html = await readFile4(htmlPath, "utf8");
2491
+ if (looksLikeAuthPage(html)) {
2492
+ throw new Error("Downloaded auth/login page instead of canvas content (token may be expired)");
2493
+ }
2494
+ const md = htmlToMarkdown(html).trim();
2495
+ const safeName = `${input.fileId.replace(/[\\/<>:"|?*]/g, "_")}.md`;
2496
+ const mdPath = join8(input.destDir, safeName);
2497
+ await writeFile3(mdPath, md, "utf8");
2498
+ return mdPath;
2499
+ }
2347
2500
  async function downloadFilesForMessages(input) {
2348
2501
  const downloadedPaths = {};
2349
2502
  const downloadsDir = await ensureDownloadsDir();
@@ -2352,18 +2505,31 @@ async function downloadFilesForMessages(input) {
2352
2505
  if (downloadedPaths[f.id]) {
2353
2506
  continue;
2354
2507
  }
2355
- const url = f.url_private_download || f.url_private;
2508
+ const isCanvas = f.mode != null && CANVAS_MODES.has(f.mode);
2509
+ const url = isCanvas ? f.url_private || f.url_private_download : f.url_private_download || f.url_private;
2356
2510
  if (!url) {
2357
2511
  continue;
2358
2512
  }
2359
- const ext = inferExt(f);
2360
- const path = await downloadSlackFile({
2361
- auth: input.auth,
2362
- url,
2363
- destDir: downloadsDir,
2364
- preferredName: `${f.id}${ext ? `.${ext}` : ""}`
2365
- });
2366
- downloadedPaths[f.id] = path;
2513
+ try {
2514
+ if (isCanvas) {
2515
+ downloadedPaths[f.id] = await downloadCanvasAsMarkdown({
2516
+ auth: input.auth,
2517
+ fileId: f.id,
2518
+ url,
2519
+ destDir: downloadsDir
2520
+ });
2521
+ } else {
2522
+ const ext = inferExt(f);
2523
+ downloadedPaths[f.id] = await downloadSlackFile({
2524
+ auth: input.auth,
2525
+ url,
2526
+ destDir: downloadsDir,
2527
+ preferredName: `${f.id}${ext ? `.${ext}` : ""}`
2528
+ });
2529
+ }
2530
+ } catch (err) {
2531
+ console.error(`Warning: skipping file ${f.id}: ${err instanceof Error ? err.message : String(err)}`);
2532
+ }
2367
2533
  }
2368
2534
  }
2369
2535
  return downloadedPaths;
@@ -2456,7 +2622,21 @@ async function handleMessageList(input) {
2456
2622
  const threadTs = input.options.threadTs?.trim();
2457
2623
  const ts = input.options.ts?.trim();
2458
2624
  if (!threadTs && !ts) {
2459
- throw new Error('When targeting a channel, you must pass --thread-ts "<seconds>.<micros>" (or --ts to resolve a message to its thread)');
2625
+ const includeReactions2 = Boolean(input.options.includeReactions);
2626
+ const limit = parseLimit(input.options.limit);
2627
+ const channelMessages = await fetchChannelHistory(client, {
2628
+ channelId,
2629
+ limit,
2630
+ latest: input.options.latest?.trim(),
2631
+ oldest: input.options.oldest?.trim(),
2632
+ includeReactions: includeReactions2
2633
+ });
2634
+ const downloadedPaths2 = await downloadFilesForMessages({ auth, messages: channelMessages });
2635
+ const maxBodyChars2 = Number.parseInt(input.options.maxBodyChars, 10);
2636
+ return pruneEmpty({
2637
+ channel_id: channelId,
2638
+ messages: channelMessages.map((m) => toCompactMessage(m, { maxBodyChars: maxBodyChars2, includeReactions: includeReactions2, downloadedPaths: downloadedPaths2 }))
2639
+ });
2460
2640
  }
2461
2641
  const rootTs = threadTs ?? await (async () => {
2462
2642
  const ref = {
@@ -2574,7 +2754,7 @@ function registerMessageCommand(input) {
2574
2754
  process.exitCode = 1;
2575
2755
  }
2576
2756
  });
2577
- 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) => {
2757
+ 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 URL (needed when using #channel/channel id and you have 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("--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) => {
2578
2758
  const [targetInput, options] = args;
2579
2759
  try {
2580
2760
  const payload = await handleMessageList({ ctx: input.ctx, targetInput, options });
@@ -3359,13 +3539,13 @@ function registerSearchCommand(input) {
3359
3539
 
3360
3540
  // src/lib/update.ts
3361
3541
  import { createHash } from "node:crypto";
3362
- import { chmod, copyFile, mkdir as mkdir5, readFile as readFile4, rename, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
3542
+ import { chmod, copyFile, mkdir as mkdir5, readFile as readFile5, rename, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
3363
3543
  import { tmpdir as tmpdir2 } from "node:os";
3364
- import { join as join8 } from "node:path";
3544
+ import { join as join9 } from "node:path";
3365
3545
  var REPO = "stablyai/agent-slack";
3366
3546
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
3367
3547
  function getCachePath() {
3368
- return join8(getAppDir(), "update-check.json");
3548
+ return join9(getAppDir(), "update-check.json");
3369
3549
  }
3370
3550
  function compareSemver(a, b) {
3371
3551
  const pa = a.replace(/^v/, "").split(".").map(Number);
@@ -3422,24 +3602,24 @@ async function checkForUpdate(force = false) {
3422
3602
  };
3423
3603
  }
3424
3604
  function detectPlatformAsset() {
3425
- const platform4 = process.platform === "win32" ? "windows" : process.platform;
3605
+ const platform5 = process.platform === "win32" ? "windows" : process.platform;
3426
3606
  const archMap = { x64: "x64", arm64: "arm64" };
3427
3607
  const arch = archMap[process.arch] ?? process.arch;
3428
- const ext = platform4 === "windows" ? ".exe" : "";
3429
- return `agent-slack-${platform4}-${arch}${ext}`;
3608
+ const ext = platform5 === "windows" ? ".exe" : "";
3609
+ return `agent-slack-${platform5}-${arch}${ext}`;
3430
3610
  }
3431
3611
  async function sha256(filePath) {
3432
- const data = await readFile4(filePath);
3612
+ const data = await readFile5(filePath);
3433
3613
  return createHash("sha256").update(data).digest("hex");
3434
3614
  }
3435
3615
  async function performUpdate(latest) {
3436
3616
  const asset = detectPlatformAsset();
3437
3617
  const tag = `v${latest}`;
3438
3618
  const baseUrl = `https://github.com/${REPO}/releases/download/${tag}`;
3439
- const tmp = join8(tmpdir2(), `agent-slack-update-${Date.now()}`);
3619
+ const tmp = join9(tmpdir2(), `agent-slack-update-${Date.now()}`);
3440
3620
  await mkdir5(tmp, { recursive: true });
3441
- const binTmp = join8(tmp, asset);
3442
- const sumsTmp = join8(tmp, "checksums-sha256.txt");
3621
+ const binTmp = join9(tmp, asset);
3622
+ const sumsTmp = join9(tmp, "checksums-sha256.txt");
3443
3623
  try {
3444
3624
  const [binResp, sumsResp] = await Promise.all([
3445
3625
  fetch(`${baseUrl}/${asset}`, { signal: AbortSignal.timeout(120000) }),
@@ -3451,9 +3631,9 @@ async function performUpdate(latest) {
3451
3631
  if (!sumsResp.ok) {
3452
3632
  return { success: false, message: `Failed to download checksums: HTTP ${sumsResp.status}` };
3453
3633
  }
3454
- await writeFile3(binTmp, Buffer.from(await binResp.arrayBuffer()));
3634
+ await writeFile4(binTmp, Buffer.from(await binResp.arrayBuffer()));
3455
3635
  const sumsText = await sumsResp.text();
3456
- await writeFile3(sumsTmp, sumsText);
3636
+ await writeFile4(sumsTmp, sumsText);
3457
3637
  const expected = sumsText.split(`
3458
3638
  `).map((line) => line.trim().split(/\s+/)).find((parts) => parts[1] === asset)?.[0];
3459
3639
  if (!expected) {
@@ -3702,5 +3882,5 @@ if (subcommand && subcommand !== "update") {
3702
3882
  backgroundUpdateCheck();
3703
3883
  }
3704
3884
 
3705
- //# debugId=09DBB443871FCCB664756E2164756E21
3885
+ //# debugId=596521D1CCB5DA7164756E2164756E21
3706
3886
  //# sourceMappingURL=index.js.map