agent-slack 0.5.5 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -229,10 +229,11 @@ async function copySqliteForRead(dbPath) {
229
229
 
230
230
  // src/auth/chromium-cookie.ts
231
231
  import { pbkdf2Sync, createDecipheriv } from "node:crypto";
232
- function decryptChromiumCookieValue(data, password, iterations) {
232
+ function decryptChromiumCookieValue(data, options) {
233
233
  if (!data || data.length === 0) {
234
234
  return "";
235
235
  }
236
+ const { password, iterations } = options;
236
237
  if (iterations < 1) {
237
238
  throw new RangeError(`iterations must be >= 1, got ${iterations}`);
238
239
  }
@@ -373,7 +374,7 @@ async function extractCookieDFromBrave() {
373
374
  const passwords = getSafeStoragePasswords();
374
375
  for (const password of passwords) {
375
376
  try {
376
- const decrypted = decryptChromiumCookieValue(data, password, 1003);
377
+ const decrypted = decryptChromiumCookieValue(data, { password, iterations: 1003 });
377
378
  const match = decrypted.match(/xoxd-[A-Za-z0-9%/+_=.-]+/);
378
379
  if (match) {
379
380
  return match[0];
@@ -529,18 +530,10 @@ function parseSlackCurlCommand(curlInput) {
529
530
 
530
531
  // src/auth/desktop.ts
531
532
  import { cp, mkdir, rm as rm2, unlink } from "node:fs/promises";
532
- import {
533
- existsSync as existsSync4,
534
- readFileSync as readFileSync2,
535
- readdirSync,
536
- copyFileSync,
537
- writeFileSync,
538
- unlinkSync
539
- } from "node:fs";
540
- import { execFileSync as execFileSync2 } from "node:child_process";
541
- import { createDecipheriv as createDecipheriv2, randomUUID } from "node:crypto";
542
- import { homedir as homedir3, platform as platform4, tmpdir as tmpdir2 } from "node:os";
543
- import { join as join5 } from "node:path";
533
+ import { existsSync as existsSync5, readdirSync, copyFileSync, unlinkSync as unlinkSync2 } from "node:fs";
534
+ import { execFileSync as execFileSync3 } from "node:child_process";
535
+ import { homedir as homedir3, platform as platform4, tmpdir as tmpdir3 } from "node:os";
536
+ import { join as join6 } from "node:path";
544
537
 
545
538
  // src/lib/leveldb-reader.ts
546
539
  import { readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
@@ -801,32 +794,152 @@ async function findKeysContaining(dir, substring) {
801
794
  return allEntries.filter((entry) => entry.key.includes(substring));
802
795
  }
803
796
 
797
+ // src/auth/desktop-crypto.ts
798
+ import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync, unlinkSync } from "node:fs";
799
+ import { execFileSync as execFileSync2 } from "node:child_process";
800
+ import { createDecipheriv as createDecipheriv2, randomUUID } from "node:crypto";
801
+ import { tmpdir as tmpdir2 } from "node:os";
802
+ import { join as join5 } from "node:path";
803
+ var IS_MACOS4 = process.platform === "darwin";
804
+ var IS_LINUX2 = process.platform === "linux";
805
+ function getSafeStoragePasswords2(prefix) {
806
+ if (IS_MACOS4) {
807
+ const keychainQueries = [
808
+ { service: "Slack Safe Storage", account: "Slack Key" },
809
+ { service: "Slack Safe Storage", account: "Slack App Store Key" },
810
+ { service: "Slack Safe Storage" },
811
+ { service: "Chrome Safe Storage" },
812
+ { service: "Chromium Safe Storage" }
813
+ ];
814
+ const passwords = [];
815
+ for (const q of keychainQueries) {
816
+ try {
817
+ const args = ["-w", "-s", q.service];
818
+ if (q.account) {
819
+ args.push("-a", q.account);
820
+ }
821
+ const out = execFileSync2("security", ["find-generic-password", ...args], {
822
+ encoding: "utf8",
823
+ stdio: ["ignore", "pipe", "ignore"]
824
+ }).trim();
825
+ if (out) {
826
+ passwords.push(out);
827
+ }
828
+ } catch {}
829
+ }
830
+ if (passwords.length > 0) {
831
+ return [...new Set(passwords)];
832
+ }
833
+ }
834
+ if (IS_LINUX2) {
835
+ const attributes = [
836
+ ["application", "com.slack.Slack"],
837
+ ["application", "Slack"],
838
+ ["application", "slack"],
839
+ ["service", "Slack Safe Storage"]
840
+ ];
841
+ const passwords = [];
842
+ for (const pair of attributes) {
843
+ try {
844
+ const out = execFileSync2("secret-tool", ["lookup", ...pair], {
845
+ encoding: "utf8",
846
+ stdio: ["ignore", "pipe", "ignore"]
847
+ }).trim();
848
+ if (out) {
849
+ passwords.push(out);
850
+ }
851
+ } catch {}
852
+ }
853
+ if (prefix === "v11") {
854
+ passwords.push("");
855
+ }
856
+ passwords.push("peanuts");
857
+ return [...new Set(passwords)];
858
+ }
859
+ throw new Error("Could not read Safe Storage password from desktop keychain.");
860
+ }
861
+ function decryptCookieWindows(encrypted, slackDataDir) {
862
+ const localStatePath = join5(slackDataDir, "Local State");
863
+ if (!existsSync4(localStatePath)) {
864
+ throw new Error(`Local State file not found: ${localStatePath}`);
865
+ }
866
+ let localState;
867
+ try {
868
+ localState = JSON.parse(readFileSync2(localStatePath, "utf8"));
869
+ } catch (error) {
870
+ throw new Error(`Failed to parse Local State file: ${localStatePath}`, { cause: error });
871
+ }
872
+ const osCrypt = isRecord(localState) ? localState.os_crypt : undefined;
873
+ if (!isRecord(osCrypt) || typeof osCrypt.encrypted_key !== "string") {
874
+ throw new Error("No os_crypt.encrypted_key in Local State");
875
+ }
876
+ const encKeyFull = Buffer.from(osCrypt.encrypted_key, "base64");
877
+ const encKeyBlob = encKeyFull.subarray(5);
878
+ const id = randomUUID();
879
+ const encKeyFile = join5(tmpdir2(), `as-key-enc-${id}.bin`);
880
+ const decKeyFile = join5(tmpdir2(), `as-key-dec-${id}.bin`);
881
+ writeFileSync(encKeyFile, encKeyBlob, { mode: 384 });
882
+ try {
883
+ const psEncKeyFile = encKeyFile.replaceAll("'", "''");
884
+ const psDecKeyFile = decKeyFile.replaceAll("'", "''");
885
+ const psCmd = [
886
+ "Add-Type -AssemblyName System.Security",
887
+ `$e=[System.IO.File]::ReadAllBytes('${psEncKeyFile}')`,
888
+ "$d=[System.Security.Cryptography.ProtectedData]::Unprotect($e,$null,[System.Security.Cryptography.DataProtectionScope]::CurrentUser)",
889
+ `[System.IO.File]::WriteAllBytes('${psDecKeyFile}',$d)`
890
+ ].join("; ");
891
+ execFileSync2("powershell", ["-ExecutionPolicy", "Bypass", "-Command", psCmd], {
892
+ stdio: "pipe"
893
+ });
894
+ if (!existsSync4(decKeyFile)) {
895
+ throw new Error("DPAPI decryption failed: PowerShell did not produce the decrypted key file");
896
+ }
897
+ const aesKey = readFileSync2(decKeyFile);
898
+ const nonce = encrypted.subarray(3, 15);
899
+ const ciphertextWithTag = encrypted.subarray(15);
900
+ const tag = ciphertextWithTag.subarray(-16);
901
+ const ciphertext = ciphertextWithTag.subarray(0, -16);
902
+ const decipher = createDecipheriv2("aes-256-gcm", aesKey, nonce);
903
+ decipher.setAuthTag(tag);
904
+ let decrypted = decipher.update(ciphertext, undefined, "utf8");
905
+ decrypted += decipher.final("utf8");
906
+ return decrypted;
907
+ } finally {
908
+ try {
909
+ unlinkSync(encKeyFile);
910
+ } catch {}
911
+ try {
912
+ unlinkSync(decKeyFile);
913
+ } catch {}
914
+ }
915
+ }
916
+
804
917
  // src/auth/desktop.ts
805
918
  var PLATFORM2 = platform4();
806
- var IS_MACOS4 = PLATFORM2 === "darwin";
807
- var IS_LINUX2 = PLATFORM2 === "linux";
919
+ var IS_MACOS5 = PLATFORM2 === "darwin";
920
+ var IS_LINUX3 = PLATFORM2 === "linux";
808
921
  var IS_WIN32 = PLATFORM2 === "win32";
809
- var SLACK_SUPPORT_DIR_ELECTRON = join5(homedir3(), "Library", "Application Support", "Slack");
810
- var SLACK_SUPPORT_DIR_APPSTORE = join5(homedir3(), "Library", "Containers", "com.tinyspeck.slackmacgap", "Data", "Library", "Application Support", "Slack");
811
- var SLACK_SUPPORT_DIR_LINUX = join5(homedir3(), ".config", "Slack");
812
- var SLACK_SUPPORT_DIR_LINUX_FLATPAK = join5(homedir3(), ".var", "app", "com.slack.Slack", "config", "Slack");
813
- var SLACK_SUPPORT_DIR_WIN_APPDATA = join5(process.env.APPDATA || join5(homedir3(), "AppData", "Roaming"), "Slack");
922
+ var SLACK_SUPPORT_DIR_ELECTRON = join6(homedir3(), "Library", "Application Support", "Slack");
923
+ var SLACK_SUPPORT_DIR_APPSTORE = join6(homedir3(), "Library", "Containers", "com.tinyspeck.slackmacgap", "Data", "Library", "Application Support", "Slack");
924
+ var SLACK_SUPPORT_DIR_LINUX = join6(homedir3(), ".config", "Slack");
925
+ var SLACK_SUPPORT_DIR_LINUX_FLATPAK = join6(homedir3(), ".var", "app", "com.slack.Slack", "config", "Slack");
926
+ var SLACK_SUPPORT_DIR_WIN_APPDATA = join6(process.env.APPDATA || join6(homedir3(), "AppData", "Roaming"), "Slack");
814
927
  function getWindowsStoreSlackPath() {
815
- const pkgBase = join5(process.env.LOCALAPPDATA || join5(homedir3(), "AppData", "Local"), "Packages");
928
+ const pkgBase = join6(process.env.LOCALAPPDATA || join6(homedir3(), "AppData", "Local"), "Packages");
816
929
  try {
817
930
  const entries = readdirSync(pkgBase);
818
931
  const slackPkg = entries.find((e) => e.startsWith("com.tinyspeck.slackdesktop_"));
819
932
  if (slackPkg) {
820
- return join5(pkgBase, slackPkg, "LocalCache", "Roaming", "Slack");
933
+ return join6(pkgBase, slackPkg, "LocalCache", "Roaming", "Slack");
821
934
  }
822
935
  } catch {}
823
936
  return null;
824
937
  }
825
938
  function getAllSlackPaths() {
826
939
  let candidates;
827
- if (IS_MACOS4) {
940
+ if (IS_MACOS5) {
828
941
  candidates = [SLACK_SUPPORT_DIR_ELECTRON, SLACK_SUPPORT_DIR_APPSTORE];
829
- } else if (IS_LINUX2) {
942
+ } else if (IS_LINUX3) {
830
943
  candidates = [SLACK_SUPPORT_DIR_LINUX_FLATPAK, SLACK_SUPPORT_DIR_LINUX];
831
944
  } else if (IS_WIN32) {
832
945
  candidates = [SLACK_SUPPORT_DIR_WIN_APPDATA];
@@ -842,16 +955,16 @@ function getAllSlackPaths() {
842
955
  }
843
956
  const results = [];
844
957
  for (const dir of candidates) {
845
- const leveldbDir = join5(dir, "Local Storage", "leveldb");
846
- if (existsSync4(leveldbDir)) {
847
- const cookiesDbCandidates = [join5(dir, "Network", "Cookies"), join5(dir, "Cookies")];
848
- const cookiesDb = cookiesDbCandidates.find((candidate) => existsSync4(candidate)) || cookiesDbCandidates[0];
958
+ const leveldbDir = join6(dir, "Local Storage", "leveldb");
959
+ if (existsSync5(leveldbDir)) {
960
+ const cookiesDbCandidates = [join6(dir, "Network", "Cookies"), join6(dir, "Cookies")];
961
+ const cookiesDb = cookiesDbCandidates.find((candidate) => existsSync5(candidate)) || cookiesDbCandidates[0];
849
962
  results.push({ leveldbDir, cookiesDb, baseDir: dir });
850
963
  }
851
964
  }
852
965
  if (results.length === 0) {
853
966
  throw new Error(`Slack Desktop data not found. Checked:
854
- - ${candidates.map((d) => join5(d, "Local Storage", "leveldb")).join(`
967
+ - ${candidates.map((d) => join6(d, "Local Storage", "leveldb")).join(`
855
968
  - `)}`);
856
969
  }
857
970
  return results;
@@ -869,13 +982,13 @@ function toDesktopTeam(value) {
869
982
  return { url, name, token };
870
983
  }
871
984
  async function snapshotLevelDb(srcDir) {
872
- const base = join5(homedir3(), ".config", "agent-slack", "cache", "leveldb-snapshots");
873
- const dest = join5(base, `${Date.now()}`);
985
+ const base = join6(homedir3(), ".config", "agent-slack", "cache", "leveldb-snapshots");
986
+ const dest = join6(base, `${Date.now()}`);
874
987
  await mkdir(base, { recursive: true });
875
- let useNodeCopy = !IS_MACOS4;
876
- if (IS_MACOS4) {
988
+ let useNodeCopy = !IS_MACOS5;
989
+ if (IS_MACOS5) {
877
990
  try {
878
- execFileSync2("cp", ["-cR", srcDir, dest], {
991
+ execFileSync3("cp", ["-cR", srcDir, dest], {
879
992
  stdio: ["ignore", "ignore", "ignore"]
880
993
  });
881
994
  } catch {
@@ -886,7 +999,7 @@ async function snapshotLevelDb(srcDir) {
886
999
  await cp(srcDir, dest, { recursive: true, force: true });
887
1000
  }
888
1001
  try {
889
- await unlink(join5(dest, "LOCK"));
1002
+ await unlink(join6(dest, "LOCK"));
890
1003
  } catch {}
891
1004
  return dest;
892
1005
  }
@@ -928,7 +1041,7 @@ function parseLocalConfig(raw) {
928
1041
  throw lastErr || new Error("localConfig not parseable");
929
1042
  }
930
1043
  async function extractTeamsFromSlackLevelDb(leveldbDir) {
931
- if (!existsSync4(leveldbDir)) {
1044
+ if (!existsSync5(leveldbDir)) {
932
1045
  throw new Error(`Slack LevelDB not found: ${leveldbDir}`);
933
1046
  }
934
1047
  const snap = await snapshotLevelDb(leveldbDir);
@@ -969,124 +1082,13 @@ async function extractTeamsFromSlackLevelDb(leveldbDir) {
969
1082
  } catch {}
970
1083
  }
971
1084
  }
972
- function getSafeStoragePasswords2(prefix) {
973
- if (IS_MACOS4) {
974
- const keychainQueries = [
975
- { service: "Slack Safe Storage", account: "Slack Key" },
976
- { service: "Slack Safe Storage", account: "Slack App Store Key" },
977
- { service: "Slack Safe Storage" },
978
- { service: "Chrome Safe Storage" },
979
- { service: "Chromium Safe Storage" }
980
- ];
981
- const passwords = [];
982
- for (const q of keychainQueries) {
983
- try {
984
- const args = ["-w", "-s", q.service];
985
- if (q.account) {
986
- args.push("-a", q.account);
987
- }
988
- const out = execFileSync2("security", ["find-generic-password", ...args], {
989
- encoding: "utf8",
990
- stdio: ["ignore", "pipe", "ignore"]
991
- }).trim();
992
- if (out) {
993
- passwords.push(out);
994
- }
995
- } catch {}
996
- }
997
- if (passwords.length > 0) {
998
- return [...new Set(passwords)];
999
- }
1000
- }
1001
- if (IS_LINUX2) {
1002
- const attributes = [
1003
- ["application", "com.slack.Slack"],
1004
- ["application", "Slack"],
1005
- ["application", "slack"],
1006
- ["service", "Slack Safe Storage"]
1007
- ];
1008
- const passwords = [];
1009
- for (const pair of attributes) {
1010
- try {
1011
- const out = execFileSync2("secret-tool", ["lookup", ...pair], {
1012
- encoding: "utf8",
1013
- stdio: ["ignore", "pipe", "ignore"]
1014
- }).trim();
1015
- if (out) {
1016
- passwords.push(out);
1017
- }
1018
- } catch {}
1019
- }
1020
- if (prefix === "v11") {
1021
- passwords.push("");
1022
- }
1023
- passwords.push("peanuts");
1024
- return [...new Set(passwords)];
1025
- }
1026
- throw new Error("Could not read Safe Storage password from desktop keychain.");
1027
- }
1028
- function decryptCookieWindows(encrypted, slackDataDir) {
1029
- const localStatePath = join5(slackDataDir, "Local State");
1030
- if (!existsSync4(localStatePath)) {
1031
- throw new Error(`Local State file not found: ${localStatePath}`);
1032
- }
1033
- let localState;
1034
- try {
1035
- localState = JSON.parse(readFileSync2(localStatePath, "utf8"));
1036
- } catch (error) {
1037
- throw new Error(`Failed to parse Local State file: ${localStatePath}`, { cause: error });
1038
- }
1039
- const osCrypt = isRecord(localState) ? localState.os_crypt : undefined;
1040
- if (!isRecord(osCrypt) || typeof osCrypt.encrypted_key !== "string") {
1041
- throw new Error("No os_crypt.encrypted_key in Local State");
1042
- }
1043
- const encKeyFull = Buffer.from(osCrypt.encrypted_key, "base64");
1044
- const encKeyBlob = encKeyFull.subarray(5);
1045
- const id = randomUUID();
1046
- const encKeyFile = join5(tmpdir2(), `as-key-enc-${id}.bin`);
1047
- const decKeyFile = join5(tmpdir2(), `as-key-dec-${id}.bin`);
1048
- writeFileSync(encKeyFile, encKeyBlob, { mode: 384 });
1049
- try {
1050
- const psEncKeyFile = encKeyFile.replaceAll("'", "''");
1051
- const psDecKeyFile = decKeyFile.replaceAll("'", "''");
1052
- const psCmd = [
1053
- "Add-Type -AssemblyName System.Security",
1054
- `$e=[System.IO.File]::ReadAllBytes('${psEncKeyFile}')`,
1055
- "$d=[System.Security.Cryptography.ProtectedData]::Unprotect($e,$null,[System.Security.Cryptography.DataProtectionScope]::CurrentUser)",
1056
- `[System.IO.File]::WriteAllBytes('${psDecKeyFile}',$d)`
1057
- ].join("; ");
1058
- execFileSync2("powershell", ["-ExecutionPolicy", "Bypass", "-Command", psCmd], {
1059
- stdio: "pipe"
1060
- });
1061
- if (!existsSync4(decKeyFile)) {
1062
- throw new Error("DPAPI decryption failed: PowerShell did not produce the decrypted key file");
1063
- }
1064
- const aesKey = readFileSync2(decKeyFile);
1065
- const nonce = encrypted.subarray(3, 15);
1066
- const ciphertextWithTag = encrypted.subarray(15);
1067
- const tag = ciphertextWithTag.subarray(-16);
1068
- const ciphertext = ciphertextWithTag.subarray(0, -16);
1069
- const decipher = createDecipheriv2("aes-256-gcm", aesKey, nonce);
1070
- decipher.setAuthTag(tag);
1071
- let decrypted = decipher.update(ciphertext, undefined, "utf8");
1072
- decrypted += decipher.final("utf8");
1073
- return decrypted;
1074
- } finally {
1075
- try {
1076
- unlinkSync(encKeyFile);
1077
- } catch {}
1078
- try {
1079
- unlinkSync(decKeyFile);
1080
- } catch {}
1081
- }
1082
- }
1083
1085
  async function extractCookieDFromSlackCookiesDb(cookiesPath, slackDataDir) {
1084
- if (!existsSync4(cookiesPath)) {
1086
+ if (!existsSync5(cookiesPath)) {
1085
1087
  throw new Error(`Slack Cookies DB not found: ${cookiesPath}`);
1086
1088
  }
1087
1089
  let dbPathToQuery = cookiesPath;
1088
1090
  if (IS_WIN32) {
1089
- const tmpCopy = join5(tmpdir2(), `agent-slack-cookies-${Date.now()}`);
1091
+ const tmpCopy = join6(tmpdir3(), `agent-slack-cookies-${Date.now()}`);
1090
1092
  copyFileSync(cookiesPath, tmpCopy);
1091
1093
  dbPathToQuery = tmpCopy;
1092
1094
  }
@@ -1096,7 +1098,7 @@ async function extractCookieDFromSlackCookiesDb(cookiesPath, slackDataDir) {
1096
1098
  } finally {
1097
1099
  if (IS_WIN32 && dbPathToQuery !== cookiesPath) {
1098
1100
  try {
1099
- unlinkSync(dbPathToQuery);
1101
+ unlinkSync2(dbPathToQuery);
1100
1102
  } catch {}
1101
1103
  }
1102
1104
  }
@@ -1128,7 +1130,10 @@ async function extractCookieDFromSlackCookiesDb(cookiesPath, slackDataDir) {
1128
1130
  const passwords = getSafeStoragePasswords2(prefix);
1129
1131
  for (const password of passwords) {
1130
1132
  try {
1131
- const decrypted = decryptChromiumCookieValue(data, password, IS_LINUX2 ? 1 : 1003);
1133
+ const decrypted = decryptChromiumCookieValue(data, {
1134
+ password,
1135
+ iterations: IS_LINUX3 ? 1 : 1003
1136
+ });
1132
1137
  const match = decrypted.match(/xoxd-[A-Za-z0-9%/+_=.-]+/);
1133
1138
  if (match) {
1134
1139
  return match[0];
@@ -1159,8 +1164,8 @@ async function extractFromSlackDesktop() {
1159
1164
  }
1160
1165
 
1161
1166
  // src/auth/firefox.ts
1162
- import { existsSync as existsSync5 } from "node:fs";
1163
- import { join as join6 } from "node:path";
1167
+ import { existsSync as existsSync6 } from "node:fs";
1168
+ import { join as join7 } from "node:path";
1164
1169
  var CONTROL_CHAR_RE = /[\u0000-\u001F]/g;
1165
1170
  function toStringValue(value) {
1166
1171
  if (typeof value === "string") {
@@ -1267,21 +1272,21 @@ function extractTeamsFromRawText(raw) {
1267
1272
  return teams;
1268
1273
  }
1269
1274
  function getLocalStorageDirs(profilePath) {
1270
- const roots = [join6(profilePath, "storage", "default")];
1275
+ const roots = [join7(profilePath, "storage", "default")];
1271
1276
  const candidates = [];
1272
1277
  for (const root of roots) {
1273
- if (!existsSync5(root)) {
1278
+ if (!existsSync6(root)) {
1274
1279
  continue;
1275
1280
  }
1276
- candidates.push(join6(root, "https+++app.slack.com", "ls"));
1281
+ candidates.push(join7(root, "https+++app.slack.com", "ls"));
1277
1282
  }
1278
1283
  return candidates;
1279
1284
  }
1280
1285
  async function extractTeamsFromProfile(profilePath) {
1281
1286
  const lsDirs = getLocalStorageDirs(profilePath);
1282
1287
  for (const lsDir of lsDirs) {
1283
- const dbPath = join6(lsDir, "data.sqlite");
1284
- if (!existsSync5(dbPath)) {
1288
+ const dbPath = join7(lsDir, "data.sqlite");
1289
+ if (!existsSync6(dbPath)) {
1285
1290
  continue;
1286
1291
  }
1287
1292
  const copied = await copySqliteForRead(dbPath);
@@ -1306,8 +1311,8 @@ async function extractTeamsFromProfile(profilePath) {
1306
1311
  return null;
1307
1312
  }
1308
1313
  async function extractCookieDFromProfile(profilePath) {
1309
- const dbPath = join6(profilePath, "cookies.sqlite");
1310
- if (!existsSync5(dbPath)) {
1314
+ const dbPath = join7(profilePath, "cookies.sqlite");
1315
+ if (!existsSync6(dbPath)) {
1311
1316
  return null;
1312
1317
  }
1313
1318
  const copied = await copySqliteForRead(dbPath);
@@ -1368,9 +1373,9 @@ async function extractFromFirefox(input) {
1368
1373
 
1369
1374
  // src/auth/paths.ts
1370
1375
  import { homedir as homedir4 } from "node:os";
1371
- import { join as join7 } from "node:path";
1372
- var AGENT_SLACK_DIR = join7(homedir4(), ".config", "agent-slack");
1373
- var CREDENTIALS_FILE = join7(AGENT_SLACK_DIR, "credentials.json");
1376
+ import { join as join8 } from "node:path";
1377
+ var AGENT_SLACK_DIR = join8(homedir4(), ".config", "agent-slack");
1378
+ var CREDENTIALS_FILE = join8(AGENT_SLACK_DIR, "credentials.json");
1374
1379
  var KEYCHAIN_SERVICE = "agent-slack";
1375
1380
 
1376
1381
  // src/lib/fs.ts
@@ -1425,31 +1430,31 @@ var CredentialsSchema = z.object({
1425
1430
 
1426
1431
  // src/auth/keychain.ts
1427
1432
  import { platform as platform5 } from "node:os";
1428
- import { execFileSync as execFileSync3 } from "node:child_process";
1429
- var IS_MACOS5 = platform5() === "darwin";
1433
+ import { execFileSync as execFileSync4 } from "node:child_process";
1434
+ var IS_MACOS6 = platform5() === "darwin";
1430
1435
  function keychainGet(account, service) {
1431
- if (!IS_MACOS5) {
1436
+ if (!IS_MACOS6) {
1432
1437
  return null;
1433
1438
  }
1434
1439
  try {
1435
- const result = execFileSync3("security", ["find-generic-password", "-s", service, "-a", account, "-w"], { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] });
1440
+ const result = execFileSync4("security", ["find-generic-password", "-s", service, "-a", account, "-w"], { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] });
1436
1441
  return result.trim() || null;
1437
1442
  } catch {
1438
1443
  return null;
1439
1444
  }
1440
1445
  }
1441
1446
  function keychainSet(input) {
1442
- if (!IS_MACOS5) {
1447
+ if (!IS_MACOS6) {
1443
1448
  return false;
1444
1449
  }
1445
1450
  const { account, value, service } = input;
1446
1451
  try {
1447
1452
  try {
1448
- execFileSync3("security", ["delete-generic-password", "-s", service, "-a", account], {
1453
+ execFileSync4("security", ["delete-generic-password", "-s", service, "-a", account], {
1449
1454
  stdio: ["pipe", "pipe", "ignore"]
1450
1455
  });
1451
1456
  } catch {}
1452
- execFileSync3("security", ["add-generic-password", "-s", service, "-a", account, "-w", value], {
1457
+ execFileSync4("security", ["add-generic-password", "-s", service, "-a", account, "-w", value], {
1453
1458
  stdio: "pipe"
1454
1459
  });
1455
1460
  return true;
@@ -1461,7 +1466,7 @@ function keychainSet(input) {
1461
1466
  // src/auth/store.ts
1462
1467
  import { platform as platform6 } from "node:os";
1463
1468
  var KEYCHAIN_PLACEHOLDER = "__KEYCHAIN__";
1464
- var IS_MACOS6 = platform6() === "darwin";
1469
+ var IS_MACOS7 = platform6() === "darwin";
1465
1470
  function normalizeWorkspaceUrl(workspaceUrl) {
1466
1471
  const u = new URL(workspaceUrl);
1467
1472
  return `${u.protocol}//${u.host}`;
@@ -1515,7 +1520,7 @@ async function saveCredentials(credentials) {
1515
1520
  }))
1516
1521
  };
1517
1522
  const filePayload = structuredClone(payload);
1518
- if (IS_MACOS6) {
1523
+ if (IS_MACOS7) {
1519
1524
  const firstBrowser = payload.workspaces.find((w) => w.auth.auth_type === "browser");
1520
1525
  let xoxdStored = false;
1521
1526
  if (firstBrowser?.auth.auth_type === "browser" && !isPlaceholderSecret(firstBrowser.auth.xoxd_cookie)) {
@@ -1752,6 +1757,10 @@ function normalizeConversationsPage(resp) {
1752
1757
  function normalizeConversationsLimit(value) {
1753
1758
  return Math.min(Math.max(value ?? 100, 1), 1000);
1754
1759
  }
1760
+ async function markConversation(client, options) {
1761
+ const { channelId, ts } = options;
1762
+ await client.api("conversations.mark", { channel: channelId, ts });
1763
+ }
1755
1764
 
1756
1765
  // src/cli/workspace-selector.ts
1757
1766
  function normalizeUrl(u) {
@@ -2406,15 +2415,23 @@ function registerAuthCommand(input) {
2406
2415
 
2407
2416
  // src/slack/files.ts
2408
2417
  import { mkdir as mkdir3, writeFile as writeFile2 } from "node:fs/promises";
2409
- import { basename, join as join8, resolve } from "node:path";
2410
- import { existsSync as existsSync6 } from "node:fs";
2418
+ import { basename, join as join9, resolve } from "node:path";
2419
+ import { existsSync as existsSync7 } from "node:fs";
2420
+ class SlackDownloadError extends Error {
2421
+ httpStatus;
2422
+ constructor(message, httpStatus) {
2423
+ super(message);
2424
+ this.httpStatus = httpStatus;
2425
+ this.name = "SlackDownloadError";
2426
+ }
2427
+ }
2411
2428
  async function downloadSlackFile(input) {
2412
2429
  const { auth, url, destDir, preferredName, options } = input;
2413
2430
  const absDir = resolve(destDir);
2414
2431
  await mkdir3(absDir, { recursive: true });
2415
2432
  const name = sanitizeFilename(preferredName || basename(new URL(url).pathname) || "file");
2416
- const path = join8(absDir, name);
2417
- if (existsSync6(path)) {
2433
+ const path = join9(absDir, name);
2434
+ if (existsSync7(path)) {
2418
2435
  return path;
2419
2436
  }
2420
2437
  const headers = {};
@@ -2426,19 +2443,54 @@ async function downloadSlackFile(input) {
2426
2443
  headers.Referer = "https://app.slack.com/";
2427
2444
  headers["User-Agent"] = getUserAgent();
2428
2445
  }
2429
- const resp = await fetch(url, { headers });
2446
+ let resp;
2447
+ try {
2448
+ resp = await fetch(url, { headers });
2449
+ } catch (err) {
2450
+ throw new SlackDownloadError(`Network error: ${err instanceof Error ? err.message : String(err)}`);
2451
+ }
2430
2452
  if (!resp.ok) {
2431
- throw new Error(`Failed to download file (${resp.status})`);
2453
+ throw new SlackDownloadError(`Failed to download file (${resp.status})`, resp.status);
2432
2454
  }
2433
2455
  const contentType = resp.headers.get("content-type") || "";
2434
2456
  if (!options?.allowHtml && contentType.includes("text/html")) {
2435
- const text = await resp.text();
2436
- throw new Error(`Downloaded HTML instead of file (auth likely failed). First bytes: ${JSON.stringify(text.slice(0, 120))}`);
2457
+ let text;
2458
+ try {
2459
+ text = await resp.text();
2460
+ } catch (err) {
2461
+ throw new SlackDownloadError(`Failed to read download response body: ${err instanceof Error ? err.message : String(err)}`, resp.status);
2462
+ }
2463
+ throw new SlackDownloadError(`Downloaded HTML instead of file (auth likely failed). First bytes: ${JSON.stringify(text.slice(0, 120))}`, resp.status);
2437
2464
  }
2438
- const buf = Buffer.from(await resp.arrayBuffer());
2465
+ let arrayBuffer;
2466
+ try {
2467
+ arrayBuffer = await resp.arrayBuffer();
2468
+ } catch (err) {
2469
+ throw new SlackDownloadError(`Failed to read download response body: ${err instanceof Error ? err.message : String(err)}`, resp.status);
2470
+ }
2471
+ const buf = Buffer.from(arrayBuffer);
2439
2472
  await writeFile2(path, buf);
2440
2473
  return path;
2441
2474
  }
2475
+ async function tryDownloadSlackFile(input) {
2476
+ try {
2477
+ const path = await downloadSlackFile(input);
2478
+ return { ok: true, path };
2479
+ } catch (err) {
2480
+ if (err instanceof SlackDownloadError) {
2481
+ return { ok: false, error: err.message, httpStatus: err.httpStatus };
2482
+ }
2483
+ throw err;
2484
+ }
2485
+ }
2486
+ async function writeDownloadErrorFile(input) {
2487
+ const absDir = resolve(input.destDir);
2488
+ await mkdir3(absDir, { recursive: true });
2489
+ const path = join9(absDir, sanitizeFilename(`${input.fileId}.download-error.txt`));
2490
+ await writeFile2(path, `${input.error}
2491
+ `, "utf8");
2492
+ return path;
2493
+ }
2442
2494
  function sanitizeFilename(name) {
2443
2495
  return name.replace(/[\\/<>:"|?*]/g, "_");
2444
2496
  }
@@ -2469,27 +2521,27 @@ function extractTag(html, tag) {
2469
2521
  }
2470
2522
 
2471
2523
  // src/lib/tmp-paths.ts
2472
- import { join as join10, resolve as resolve2 } from "node:path";
2524
+ import { join as join11, resolve as resolve2 } from "node:path";
2473
2525
  import { mkdir as mkdir4 } from "node:fs/promises";
2474
2526
 
2475
2527
  // src/lib/app-dir.ts
2476
- import { homedir as homedir5, tmpdir as tmpdir3 } from "node:os";
2477
- import { join as join9 } from "node:path";
2528
+ import { homedir as homedir5, tmpdir as tmpdir4 } from "node:os";
2529
+ import { join as join10 } from "node:path";
2478
2530
  function getAppDir() {
2479
2531
  const xdg = process.env.XDG_RUNTIME_DIR?.trim();
2480
2532
  if (xdg) {
2481
- return join9(xdg, "agent-slack");
2533
+ return join10(xdg, "agent-slack");
2482
2534
  }
2483
2535
  const home = homedir5();
2484
2536
  if (home) {
2485
- return join9(home, ".agent-slack");
2537
+ return join10(home, ".agent-slack");
2486
2538
  }
2487
- return join9(tmpdir3(), "agent-slack");
2539
+ return join10(tmpdir4(), "agent-slack");
2488
2540
  }
2489
2541
 
2490
2542
  // src/lib/tmp-paths.ts
2491
2543
  function getDownloadsDir() {
2492
- return resolve2(join10(getAppDir(), "tmp", "downloads"));
2544
+ return resolve2(join11(getAppDir(), "tmp", "downloads"));
2493
2545
  }
2494
2546
  async function ensureDownloadsDir() {
2495
2547
  const dir = getDownloadsDir();
@@ -3120,14 +3172,16 @@ function toCompactMessage(msg, input) {
3120
3172
  const content = maxBodyChars >= 0 && rendered.length > maxBodyChars ? `${rendered.slice(0, maxBodyChars)}
3121
3173
  …` : rendered;
3122
3174
  const files = msg.files?.map((f) => {
3123
- const path = input?.downloadedPaths?.[f.id];
3124
- if (!path) {
3175
+ const entry = input?.downloadedPaths?.[f.id];
3176
+ if (!entry) {
3125
3177
  return null;
3126
3178
  }
3127
- return {
3179
+ return entry.ok ? { name: f.name, mimetype: f.mimetype, mode: f.mode, path: entry.path } : {
3180
+ name: f.name,
3128
3181
  mimetype: f.mimetype,
3129
3182
  mode: f.mode,
3130
- path
3183
+ path: entry.path,
3184
+ error: entry.error
3131
3185
  };
3132
3186
  }).filter((f) => Boolean(f));
3133
3187
  return {
@@ -3691,7 +3745,7 @@ async function uploadLocalFileToSlack(input) {
3691
3745
 
3692
3746
  // src/cli/message-file-downloads.ts
3693
3747
  import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
3694
- import { join as join11 } from "node:path";
3748
+ import { join as join12 } from "node:path";
3695
3749
  function inferFileExtension(file) {
3696
3750
  const mt = (file.mimetype || "").toLowerCase();
3697
3751
  const ft = (file.filetype || "").toLowerCase();
@@ -3734,11 +3788,11 @@ async function downloadCanvasAsMarkdown(input) {
3734
3788
  });
3735
3789
  const html = await readFile6(htmlPath, "utf8");
3736
3790
  if (looksLikeAuthPage(html)) {
3737
- throw new Error("Downloaded auth/login page instead of canvas content (token may be expired)");
3791
+ throw new SlackDownloadError("Downloaded auth/login page instead of canvas content (token may be expired)");
3738
3792
  }
3739
3793
  const markdown = htmlToMarkdown(html).trim();
3740
3794
  const safeName = `${input.fileId.replace(/[\\/<>"|?*]/g, "_")}.md`;
3741
- const markdownPath = join11(input.destDir, safeName);
3795
+ const markdownPath = join12(input.destDir, safeName);
3742
3796
  await writeFile3(markdownPath, markdown, "utf8");
3743
3797
  return markdownPath;
3744
3798
  }
@@ -3755,25 +3809,53 @@ async function downloadMessageFiles(input) {
3755
3809
  if (!url) {
3756
3810
  continue;
3757
3811
  }
3758
- try {
3759
- if (isCanvas) {
3760
- downloadedPaths[file.id] = await downloadCanvasAsMarkdown({
3812
+ if (isCanvas) {
3813
+ try {
3814
+ const path = await downloadCanvasAsMarkdown({
3761
3815
  auth: input.auth,
3762
3816
  fileId: file.id,
3763
3817
  url,
3764
3818
  destDir: downloadsDir
3765
3819
  });
3766
- } else {
3767
- const ext = inferFileExtension(file);
3768
- downloadedPaths[file.id] = await downloadSlackFile({
3769
- auth: input.auth,
3770
- url,
3820
+ downloadedPaths[file.id] = { ok: true, path };
3821
+ } catch (err) {
3822
+ if (!(err instanceof SlackDownloadError)) {
3823
+ throw err;
3824
+ }
3825
+ const path = await writeDownloadErrorFile({
3771
3826
  destDir: downloadsDir,
3772
- preferredName: `${file.id}${ext ? `.${ext}` : ""}`
3827
+ fileId: file.id,
3828
+ error: err.message
3773
3829
  });
3830
+ downloadedPaths[file.id] = {
3831
+ ok: false,
3832
+ error: err.message,
3833
+ httpStatus: err.httpStatus,
3834
+ path
3835
+ };
3836
+ console.error(`Warning: skipping file ${file.id}: ${err.message}`);
3837
+ }
3838
+ } else {
3839
+ const ext = inferFileExtension(file);
3840
+ const result = await tryDownloadSlackFile({
3841
+ auth: input.auth,
3842
+ url,
3843
+ destDir: downloadsDir,
3844
+ preferredName: `${file.id}${ext ? `.${ext}` : ""}`
3845
+ });
3846
+ if (!result.ok) {
3847
+ downloadedPaths[file.id] = {
3848
+ ...result,
3849
+ path: await writeDownloadErrorFile({
3850
+ destDir: downloadsDir,
3851
+ fileId: file.id,
3852
+ error: result.error
3853
+ })
3854
+ };
3855
+ console.error(`Warning: skipping file ${file.id}: ${result.error}`);
3856
+ } else {
3857
+ downloadedPaths[file.id] = result;
3774
3858
  }
3775
- } catch (err) {
3776
- console.error(`Warning: skipping file ${file.id}: ${err instanceof Error ? err.message : String(err)}`);
3777
3859
  }
3778
3860
  }
3779
3861
  }
@@ -6605,18 +6687,22 @@ async function searchFilesViaSearchApi(client, input) {
6605
6687
  if (!id) {
6606
6688
  continue;
6607
6689
  }
6608
- const path = await downloadSlackFile({
6690
+ const result = await tryDownloadSlackFile({
6609
6691
  auth: input.auth,
6610
6692
  url,
6611
6693
  destDir: downloadsDir,
6612
6694
  preferredName: `${id}${ext ? `.${ext}` : ""}`
6613
6695
  });
6696
+ if (!result.ok) {
6697
+ console.warn(`Warning: skipping file ${id}: ${result.error}`);
6698
+ continue;
6699
+ }
6614
6700
  const title = (getString(f.title) || getString(f.name) || "").trim();
6615
6701
  out.push({
6616
6702
  title: title || undefined,
6617
6703
  mimetype,
6618
6704
  mode,
6619
- path
6705
+ path: result.path
6620
6706
  });
6621
6707
  if (out.length >= input.limit) {
6622
6708
  break;
@@ -6671,17 +6757,21 @@ async function searchFilesInChannelsFallback(client, input) {
6671
6757
  if (!id) {
6672
6758
  continue;
6673
6759
  }
6674
- const path = await downloadSlackFile({
6760
+ const result = await tryDownloadSlackFile({
6675
6761
  auth: input.auth,
6676
6762
  url,
6677
6763
  destDir: downloadsDir,
6678
6764
  preferredName: `${id}${ext ? `.${ext}` : ""}`
6679
6765
  });
6766
+ if (!result.ok) {
6767
+ console.warn(`Warning: skipping file ${id}: ${result.error}`);
6768
+ continue;
6769
+ }
6680
6770
  out.push({
6681
6771
  title: title || undefined,
6682
6772
  mimetype,
6683
6773
  mode,
6684
- path
6774
+ path: result.path
6685
6775
  });
6686
6776
  if (out.length >= input.limit) {
6687
6777
  return out;
@@ -6897,13 +6987,25 @@ async function downloadFilesForMessage(input) {
6897
6987
  continue;
6898
6988
  }
6899
6989
  const ext = inferExt(f);
6900
- const path = await downloadSlackFile({
6990
+ const result = await tryDownloadSlackFile({
6901
6991
  auth: input.auth,
6902
6992
  url,
6903
6993
  destDir: input.downloadsDir,
6904
6994
  preferredName: `${f.id}${ext ? `.${ext}` : ""}`
6905
6995
  });
6906
- input.downloadedPaths[f.id] = path;
6996
+ if (!result.ok) {
6997
+ input.downloadedPaths[f.id] = {
6998
+ ...result,
6999
+ path: await writeDownloadErrorFile({
7000
+ destDir: input.downloadsDir,
7001
+ fileId: f.id,
7002
+ error: result.error
7003
+ })
7004
+ };
7005
+ console.warn(`Warning: file ${f.id}: ${result.error}`);
7006
+ } else {
7007
+ input.downloadedPaths[f.id] = result;
7008
+ }
6907
7009
  }
6908
7010
  }
6909
7011
  function messageSummaryFromApiMessage(channelId, msg) {
@@ -7073,12 +7175,12 @@ function registerSearchCommand(input) {
7073
7175
  import { execSync as execSync2 } from "node:child_process";
7074
7176
  import { createHash } from "node:crypto";
7075
7177
  import { chmod, copyFile as copyFile2, mkdir as mkdir5, readFile as readFile7, rename, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
7076
- import { tmpdir as tmpdir4 } from "node:os";
7077
- import { basename as basename3, join as join12 } from "node:path";
7178
+ import { tmpdir as tmpdir5 } from "node:os";
7179
+ import { basename as basename3, join as join13 } from "node:path";
7078
7180
  var REPO = "stablyai/agent-slack";
7079
7181
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
7080
7182
  function getCachePath() {
7081
- return join12(getAppDir(), "update-check.json");
7183
+ return join13(getAppDir(), "update-check.json");
7082
7184
  }
7083
7185
  function compareSemver(a, b) {
7084
7186
  const pa = a.replace(/^v/, "").split(".").map(Number);
@@ -7180,10 +7282,10 @@ async function performUpdate(latest) {
7180
7282
  const asset = detectPlatformAsset();
7181
7283
  const tag = `v${latest}`;
7182
7284
  const baseUrl = `https://github.com/${REPO}/releases/download/${tag}`;
7183
- const tmp = join12(tmpdir4(), `agent-slack-update-${Date.now()}`);
7285
+ const tmp = join13(tmpdir5(), `agent-slack-update-${Date.now()}`);
7184
7286
  await mkdir5(tmp, { recursive: true });
7185
- const binTmp = join12(tmp, asset);
7186
- const sumsTmp = join12(tmp, "checksums-sha256.txt");
7287
+ const binTmp = join13(tmp, asset);
7288
+ const sumsTmp = join13(tmp, "checksums-sha256.txt");
7187
7289
  try {
7188
7290
  const [binResp, sumsResp] = await Promise.all([
7189
7291
  fetch(`${baseUrl}/${asset}`, { signal: AbortSignal.timeout(120000) }),
@@ -7774,6 +7876,49 @@ function registerChannelCommand(input) {
7774
7876
  process.exitCode = 1;
7775
7877
  }
7776
7878
  });
7879
+ channelCmd.command("mark").description("Mark a channel/DM as read up to a given message").argument("<target>", "Slack message URL, #channel, or channel ID").option("--ts <ts>", "Message ts to mark as read (required when target is a channel name/ID)").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if you have multiple workspaces)").action(async (...args) => {
7880
+ const [targetArg, options] = args;
7881
+ try {
7882
+ const target = parseMsgTarget(targetArg);
7883
+ let channelId;
7884
+ let ts;
7885
+ let workspaceUrl;
7886
+ if (target.kind === "url") {
7887
+ if (options.workspace) {
7888
+ throw new Error("--workspace cannot be used with a URL target; the workspace is derived from the URL");
7889
+ }
7890
+ channelId = target.ref.channel_id;
7891
+ ts = options.ts ?? target.ref.message_ts;
7892
+ workspaceUrl = input.ctx.effectiveWorkspaceUrl(target.ref.workspace_url);
7893
+ } else if (target.kind === "channel") {
7894
+ if (!options.ts) {
7895
+ throw new Error("--ts is required when target is a channel name or ID");
7896
+ }
7897
+ ({ ts } = options);
7898
+ workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
7899
+ channelId = target.channel;
7900
+ } else {
7901
+ throw new Error("User targets are not supported for channel mark");
7902
+ }
7903
+ await input.ctx.assertWorkspaceSpecifiedForChannelNames({
7904
+ workspaceUrl,
7905
+ channels: [channelId]
7906
+ });
7907
+ const resolvedId = await input.ctx.withAutoRefresh({
7908
+ workspaceUrl,
7909
+ work: async () => {
7910
+ const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
7911
+ const resolved = await resolveChannelId(client, channelId);
7912
+ await markConversation(client, { channelId: resolved, ts });
7913
+ return resolved;
7914
+ }
7915
+ });
7916
+ console.log(JSON.stringify({ ok: true, channel: resolvedId, ts }, null, 2));
7917
+ } catch (err) {
7918
+ console.error(input.ctx.errorMessage(err));
7919
+ process.exitCode = 1;
7920
+ }
7921
+ });
7777
7922
  }
7778
7923
 
7779
7924
  // src/index.ts
@@ -7796,5 +7941,5 @@ if (subcommand && subcommand !== "update") {
7796
7941
  backgroundUpdateCheck();
7797
7942
  }
7798
7943
 
7799
- //# debugId=0E45A4F2F731E07C64756E2164756E21
7944
+ //# debugId=3EBC31FAC3ADA2FD64756E2164756E21
7800
7945
  //# sourceMappingURL=index.js.map