agent-slack 0.4.4 → 0.5.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
@@ -48,17 +48,190 @@ function getUserAgent() {
48
48
  }
49
49
 
50
50
  // src/auth/brave.ts
51
- import { execSync, execFileSync } from "node:child_process";
52
- import { existsSync as existsSync2 } from "node:fs";
51
+ import { execFileSync } from "node:child_process";
52
+ import { existsSync as existsSync3 } from "node:fs";
53
53
  import { pbkdf2Sync, createDecipheriv } from "node:crypto";
54
+ import { homedir as homedir2, platform as platform2 } from "node:os";
55
+ import { join as join3 } from "node:path";
56
+
57
+ // src/auth/firefox-profile.ts
58
+ import { copyFile, mkdtemp, readdir, readFile, rm } from "node:fs/promises";
59
+ import { existsSync as existsSync2 } from "node:fs";
60
+ import { tmpdir } from "node:os";
54
61
  import { homedir, platform } from "node:os";
55
62
  import { join as join2 } from "node:path";
56
- var IS_MACOS = platform() === "darwin";
57
- function escapeOsaScript(script) {
58
- return script.replace(/'/g, `'"'"'`);
63
+ var PLATFORM = platform();
64
+ var IS_MACOS = PLATFORM === "darwin";
65
+ var IS_LINUX = PLATFORM === "linux";
66
+ function isMissingBunSqliteModule(error) {
67
+ if (!error || typeof error !== "object") {
68
+ return false;
69
+ }
70
+ const err = error;
71
+ const code = typeof err.code === "string" ? err.code : "";
72
+ const message = typeof err.message === "string" ? err.message : "";
73
+ if (code === "ERR_MODULE_NOT_FOUND" || code === "ERR_UNSUPPORTED_ESM_URL_SCHEME") {
74
+ return true;
75
+ }
76
+ if (!message.includes("bun:sqlite")) {
77
+ return false;
78
+ }
79
+ return message.includes("Cannot find module") || message.includes("Unknown builtin module") || message.includes("unsupported URL scheme") || message.includes("Only URLs with a scheme in");
80
+ }
81
+ async function queryReadonlySqlite(dbPath, sql) {
82
+ try {
83
+ const { Database } = await import("bun:sqlite");
84
+ const db = new Database(dbPath, { readonly: true });
85
+ try {
86
+ return db.query(sql).all();
87
+ } finally {
88
+ db.close();
89
+ }
90
+ } catch (error) {
91
+ if (!isMissingBunSqliteModule(error)) {
92
+ throw error;
93
+ }
94
+ const { DatabaseSync } = await import("node:sqlite");
95
+ const db = new DatabaseSync(dbPath, { readOnly: true });
96
+ try {
97
+ return db.prepare(sql).all();
98
+ } finally {
99
+ db.close();
100
+ }
101
+ }
102
+ }
103
+ function getFirefoxBaseDir() {
104
+ if (IS_LINUX) {
105
+ return join2(homedir(), ".mozilla", "firefox");
106
+ }
107
+ if (IS_MACOS) {
108
+ return join2(homedir(), "Library", "Application Support", "Firefox");
109
+ }
110
+ throw new Error(`Firefox extraction is not supported on ${PLATFORM}.`);
111
+ }
112
+ function parseProfilesIni(raw, baseDir) {
113
+ const lines = raw.split(/\r?\n/);
114
+ const profiles = [];
115
+ const installDefaults = new Set;
116
+ let section = "";
117
+ let current = null;
118
+ for (const lineRaw of lines) {
119
+ const line = lineRaw.trim();
120
+ if (!line || line.startsWith(";") || line.startsWith("#")) {
121
+ continue;
122
+ }
123
+ if (line.startsWith("[") && line.endsWith("]")) {
124
+ if (current) {
125
+ profiles.push(current);
126
+ current = null;
127
+ }
128
+ section = line.slice(1, -1);
129
+ if (section.startsWith("Profile")) {
130
+ current = { isRelative: true, isDefault: false };
131
+ }
132
+ continue;
133
+ }
134
+ const idx = line.indexOf("=");
135
+ if (idx === -1) {
136
+ continue;
137
+ }
138
+ const key = line.slice(0, idx).trim();
139
+ const value = line.slice(idx + 1).trim();
140
+ if (section.startsWith("Profile") && current) {
141
+ if (key === "Name") {
142
+ current.name = value;
143
+ } else if (key === "Path") {
144
+ current.path = value;
145
+ } else if (key === "IsRelative") {
146
+ current.isRelative = value !== "0";
147
+ } else if (key === "Default") {
148
+ current.isDefault = value === "1";
149
+ }
150
+ continue;
151
+ }
152
+ if (section.startsWith("Install") && key === "Default" && value) {
153
+ installDefaults.add(value);
154
+ }
155
+ }
156
+ if (current) {
157
+ profiles.push(current);
158
+ }
159
+ return profiles.filter((p) => Boolean(p.path)).map((p) => {
160
+ const profilePath = p.isRelative ? join2(baseDir, p.path) : p.path;
161
+ return {
162
+ name: p.name,
163
+ path: profilePath,
164
+ isDefault: p.isDefault || installDefaults.has(p.path)
165
+ };
166
+ });
167
+ }
168
+ async function listProfileCandidates() {
169
+ const baseDir = getFirefoxBaseDir();
170
+ const iniPath = join2(baseDir, "profiles.ini");
171
+ const candidates = [];
172
+ if (existsSync2(iniPath)) {
173
+ const raw = await readFile(iniPath, "utf8");
174
+ candidates.push(...parseProfilesIni(raw, baseDir));
175
+ }
176
+ const dirName = IS_MACOS ? "Profiles" : baseDir;
177
+ if (existsSync2(dirName)) {
178
+ const entries = await readdir(dirName, { withFileTypes: true });
179
+ for (const entry of entries) {
180
+ if (!entry.isDirectory()) {
181
+ continue;
182
+ }
183
+ const profilePath = join2(dirName, entry.name);
184
+ if (!candidates.some((c) => c.path === profilePath)) {
185
+ candidates.push({ path: profilePath, isDefault: false });
186
+ }
187
+ }
188
+ }
189
+ const existing = candidates.filter((c) => existsSync2(c.path));
190
+ existing.sort((a, b) => Number(b.isDefault) - Number(a.isDefault));
191
+ return existing;
192
+ }
193
+ function pickCandidatesByProfile(candidates, profile) {
194
+ const selector = profile?.trim();
195
+ if (!selector) {
196
+ return candidates;
197
+ }
198
+ const normalized = selector.toLowerCase();
199
+ const matched = candidates.filter((c) => {
200
+ const name = c.name?.toLowerCase() ?? "";
201
+ const base = c.path.split("/").pop()?.toLowerCase() ?? "";
202
+ const full = c.path.toLowerCase();
203
+ return name === normalized || base === normalized || full.includes(normalized);
204
+ });
205
+ return matched;
206
+ }
207
+ async function copySqliteForRead(dbPath) {
208
+ const tmpPath = await mkdtemp(join2(tmpdir(), "agent-slack-firefox-"));
209
+ const base = dbPath.split("/").pop() || "db.sqlite";
210
+ const copyPath = join2(tmpPath, base);
211
+ await copyFile(dbPath, copyPath);
212
+ for (const suffix of ["-wal", "-shm"]) {
213
+ const sidecar = `${dbPath}${suffix}`;
214
+ if (!existsSync2(sidecar)) {
215
+ continue;
216
+ }
217
+ try {
218
+ await copyFile(sidecar, `${copyPath}${suffix}`);
219
+ } catch {}
220
+ }
221
+ return {
222
+ copyPath,
223
+ cleanup: async () => {
224
+ try {
225
+ await rm(tmpPath, { recursive: true, force: true });
226
+ } catch {}
227
+ }
228
+ };
59
229
  }
230
+
231
+ // src/auth/brave.ts
232
+ var IS_MACOS2 = platform2() === "darwin";
60
233
  function osascript(script) {
61
- return execSync(`osascript -e '${escapeOsaScript(script)}'`, {
234
+ return execFileSync("osascript", ["-e", script], {
62
235
  encoding: "utf8",
63
236
  timeout: 7000,
64
237
  stdio: ["ignore", "pipe", "pipe"]
@@ -111,44 +284,7 @@ function extractTeamsFromBraveTab() {
111
284
  const teamsRecord = isRecord(teamsObj) ? teamsObj : {};
112
285
  return Object.values(teamsRecord).map((t) => toBraveTeam(t)).filter((t) => t !== null);
113
286
  }
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
- }
287
+ var BRAVE_COOKIES_DB = join3(homedir2(), "Library", "Application Support", "BraveSoftware", "Brave-Browser", "Default", "Cookies");
152
288
  function getSafeStoragePasswords() {
153
289
  const services = [
154
290
  "Brave Safe Storage",
@@ -201,7 +337,7 @@ function decryptChromiumCookieValue(data, password) {
201
337
  }
202
338
  }
203
339
  async function extractCookieDFromBrave() {
204
- if (!existsSync2(BRAVE_COOKIES_DB)) {
340
+ if (!existsSync3(BRAVE_COOKIES_DB)) {
205
341
  throw new Error(`Brave Cookies DB not found: ${BRAVE_COOKIES_DB}`);
206
342
  }
207
343
  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");
@@ -231,7 +367,7 @@ async function extractCookieDFromBrave() {
231
367
  throw new Error("Could not decrypt Slack 'd' cookie from Brave");
232
368
  }
233
369
  async function extractFromBrave() {
234
- if (!IS_MACOS) {
370
+ if (!IS_MACOS2) {
235
371
  return null;
236
372
  }
237
373
  try {
@@ -250,14 +386,14 @@ async function extractFromBrave() {
250
386
  }
251
387
 
252
388
  // 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) {
389
+ import { execSync } from "node:child_process";
390
+ import { platform as platform3 } from "node:os";
391
+ var IS_MACOS3 = platform3() === "darwin";
392
+ function escapeOsaScript(script) {
257
393
  return script.replace(/'/g, `'"'"'`);
258
394
  }
259
395
  function osascript2(script) {
260
- return execSync2(`osascript -e '${escapeOsaScript2(script)}'`, {
396
+ return execSync(`osascript -e '${escapeOsaScript(script)}'`, {
261
397
  encoding: "utf8",
262
398
  timeout: 7000,
263
399
  stdio: ["ignore", "pipe", "pipe"]
@@ -314,7 +450,7 @@ function teamsScript2() {
314
450
  `;
315
451
  }
316
452
  function extractFromChrome() {
317
- if (!IS_MACOS2) {
453
+ if (!IS_MACOS3) {
318
454
  return null;
319
455
  }
320
456
  try {
@@ -376,16 +512,16 @@ function parseSlackCurlCommand(curlInput) {
376
512
  }
377
513
 
378
514
  // src/auth/desktop.ts
379
- import { cp, mkdir, rm, unlink } from "node:fs/promises";
380
- import { existsSync as existsSync3 } from "node:fs";
515
+ import { cp, mkdir, rm as rm2, unlink } from "node:fs/promises";
516
+ import { existsSync as existsSync4 } from "node:fs";
381
517
  import { execFileSync as execFileSync2 } from "node:child_process";
382
518
  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";
519
+ import { homedir as homedir3, platform as platform4 } from "node:os";
520
+ import { join as join5 } from "node:path";
385
521
 
386
522
  // src/lib/leveldb-reader.ts
387
- import { readdir, readFile } from "node:fs/promises";
388
- import { join as join3 } from "node:path";
523
+ import { readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
524
+ import { join as join4 } from "node:path";
389
525
  import { snappyUncompress } from "hysnappy";
390
526
  var LEVELDB_MAGIC = Buffer.from([87, 251, 128, 139, 36, 117, 71, 219]);
391
527
  var COMPRESSION_NONE = 0;
@@ -489,7 +625,7 @@ async function parseSSTable(filePath) {
489
625
  const entries = [];
490
626
  let data;
491
627
  try {
492
- data = await readFile(filePath);
628
+ data = await readFile2(filePath);
493
629
  } catch {
494
630
  return entries;
495
631
  }
@@ -541,7 +677,7 @@ async function parseLogFile(filePath) {
541
677
  const entries = [];
542
678
  let data;
543
679
  try {
544
- data = await readFile(filePath);
680
+ data = await readFile2(filePath);
545
681
  } catch {
546
682
  return entries;
547
683
  }
@@ -621,18 +757,18 @@ async function readChromiumLevelDB(dir) {
621
757
  const entries = [];
622
758
  let files;
623
759
  try {
624
- files = await readdir(dir);
760
+ files = await readdir2(dir);
625
761
  } catch {
626
762
  return entries;
627
763
  }
628
764
  const sstFiles = files.filter((f) => f.endsWith(".ldb") || f.endsWith(".sst"));
629
765
  for (const file of sstFiles) {
630
- const fileEntries = await parseSSTable(join3(dir, file));
766
+ const fileEntries = await parseSSTable(join4(dir, file));
631
767
  entries.push(...fileEntries);
632
768
  }
633
769
  const logFiles = files.filter((f) => f.endsWith(".log"));
634
770
  for (const file of logFiles) {
635
- const fileEntries = await parseLogFile(join3(dir, file));
771
+ const fileEntries = await parseLogFile(join4(dir, file));
636
772
  entries.push(...fileEntries);
637
773
  }
638
774
  return entries;
@@ -680,28 +816,28 @@ async function queryReadonlySqlite2(dbPath, sql) {
680
816
  }
681
817
  }
682
818
  }
683
- var PLATFORM = platform3();
684
- var IS_MACOS3 = PLATFORM === "darwin";
685
- var IS_LINUX = PLATFORM === "linux";
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");
819
+ var PLATFORM2 = platform4();
820
+ var IS_MACOS4 = PLATFORM2 === "darwin";
821
+ var IS_LINUX2 = PLATFORM2 === "linux";
822
+ var SLACK_SUPPORT_DIR_ELECTRON = join5(homedir3(), "Library", "Application Support", "Slack");
823
+ var SLACK_SUPPORT_DIR_APPSTORE = join5(homedir3(), "Library", "Containers", "com.tinyspeck.slackmacgap", "Data", "Library", "Application Support", "Slack");
824
+ var SLACK_SUPPORT_DIR_LINUX = join5(homedir3(), ".config", "Slack");
825
+ var SLACK_SUPPORT_DIR_LINUX_FLATPAK = join5(homedir3(), ".var", "app", "com.slack.Slack", "config", "Slack");
690
826
  function getSlackPaths() {
691
- const candidates = IS_MACOS3 ? [SLACK_SUPPORT_DIR_ELECTRON, SLACK_SUPPORT_DIR_APPSTORE] : IS_LINUX ? [SLACK_SUPPORT_DIR_LINUX_FLATPAK, SLACK_SUPPORT_DIR_LINUX] : [];
827
+ const candidates = IS_MACOS4 ? [SLACK_SUPPORT_DIR_ELECTRON, SLACK_SUPPORT_DIR_APPSTORE] : IS_LINUX2 ? [SLACK_SUPPORT_DIR_LINUX_FLATPAK, SLACK_SUPPORT_DIR_LINUX] : [];
692
828
  if (candidates.length === 0) {
693
- throw new Error(`Slack Desktop extraction is not supported on ${PLATFORM}.`);
829
+ throw new Error(`Slack Desktop extraction is not supported on ${PLATFORM2}.`);
694
830
  }
695
831
  for (const dir of candidates) {
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];
832
+ const leveldbDir = join5(dir, "Local Storage", "leveldb");
833
+ if (existsSync4(leveldbDir)) {
834
+ const cookiesDbCandidates = [join5(dir, "Network", "Cookies"), join5(dir, "Cookies")];
835
+ const cookiesDb = cookiesDbCandidates.find((candidate) => existsSync4(candidate)) || cookiesDbCandidates[0];
700
836
  return { leveldbDir, cookiesDb };
701
837
  }
702
838
  }
703
839
  throw new Error(`Slack Desktop data not found. Checked:
704
- - ${candidates.map((d) => join4(d, "Local Storage", "leveldb")).join(`
840
+ - ${candidates.map((d) => join5(d, "Local Storage", "leveldb")).join(`
705
841
  - `)}`);
706
842
  }
707
843
  function isRecord3(value) {
@@ -720,11 +856,11 @@ function toDesktopTeam(value) {
720
856
  return { url, name, token };
721
857
  }
722
858
  async function snapshotLevelDb(srcDir) {
723
- const base = join4(homedir2(), ".config", "agent-slack", "cache", "leveldb-snapshots");
724
- const dest = join4(base, `${Date.now()}`);
859
+ const base = join5(homedir3(), ".config", "agent-slack", "cache", "leveldb-snapshots");
860
+ const dest = join5(base, `${Date.now()}`);
725
861
  await mkdir(base, { recursive: true });
726
- let useNodeCopy = !IS_MACOS3;
727
- if (IS_MACOS3) {
862
+ let useNodeCopy = !IS_MACOS4;
863
+ if (IS_MACOS4) {
728
864
  try {
729
865
  execFileSync2("cp", ["-cR", srcDir, dest], {
730
866
  stdio: ["ignore", "ignore", "ignore"]
@@ -737,7 +873,7 @@ async function snapshotLevelDb(srcDir) {
737
873
  await cp(srcDir, dest, { recursive: true, force: true });
738
874
  }
739
875
  try {
740
- await unlink(join4(dest, "LOCK"));
876
+ await unlink(join5(dest, "LOCK"));
741
877
  } catch {}
742
878
  return dest;
743
879
  }
@@ -779,7 +915,7 @@ function parseLocalConfig(raw) {
779
915
  throw lastErr || new Error("localConfig not parseable");
780
916
  }
781
917
  async function extractTeamsFromSlackLevelDb(leveldbDir) {
782
- if (!existsSync3(leveldbDir)) {
918
+ if (!existsSync4(leveldbDir)) {
783
919
  throw new Error(`Slack LevelDB not found: ${leveldbDir}`);
784
920
  }
785
921
  const snap = await snapshotLevelDb(leveldbDir);
@@ -816,12 +952,12 @@ async function extractTeamsFromSlackLevelDb(leveldbDir) {
816
952
  return teams;
817
953
  } finally {
818
954
  try {
819
- await rm(snap, { recursive: true, force: true });
955
+ await rm2(snap, { recursive: true, force: true });
820
956
  } catch {}
821
957
  }
822
958
  }
823
959
  function getSafeStoragePasswords2(prefix) {
824
- if (IS_MACOS3) {
960
+ if (IS_MACOS4) {
825
961
  const services = ["Slack Safe Storage", "Chrome Safe Storage", "Chromium Safe Storage"];
826
962
  const passwords = [];
827
963
  for (const service of services) {
@@ -839,7 +975,7 @@ function getSafeStoragePasswords2(prefix) {
839
975
  return passwords;
840
976
  }
841
977
  }
842
- if (IS_LINUX) {
978
+ if (IS_LINUX2) {
843
979
  const attributes = [
844
980
  ["application", "com.slack.Slack"],
845
981
  ["application", "Slack"],
@@ -872,7 +1008,7 @@ function decryptChromiumCookieValue2(data, password) {
872
1008
  }
873
1009
  const salt = Buffer.from("saltysalt", "utf8");
874
1010
  const iv = Buffer.alloc(16, " ");
875
- const key = pbkdf2Sync2(password, salt, IS_LINUX ? 1 : 1003, 16, "sha1");
1011
+ const key = pbkdf2Sync2(password, salt, IS_LINUX2 ? 1 : 1003, 16, "sha1");
876
1012
  const decipher = createDecipheriv2("aes-128-cbc", key, iv);
877
1013
  decipher.setAutoPadding(true);
878
1014
  const plain = Buffer.concat([decipher.update(data), decipher.final()]);
@@ -897,7 +1033,7 @@ function decryptChromiumCookieValue2(data, password) {
897
1033
  }
898
1034
  }
899
1035
  async function extractCookieDFromSlackCookiesDb(cookiesPath) {
900
- if (!existsSync3(cookiesPath)) {
1036
+ if (!existsSync4(cookiesPath)) {
901
1037
  throw new Error(`Slack Cookies DB not found: ${cookiesPath}`);
902
1038
  }
903
1039
  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");
@@ -938,196 +1074,27 @@ async function extractFromSlackDesktop() {
938
1074
  }
939
1075
 
940
1076
  // src/auth/firefox.ts
941
- import { copyFile, mkdtemp, readdir as readdir2, readFile as readFile2, rm as rm2 } from "node:fs/promises";
942
- import { existsSync as existsSync4 } from "node:fs";
943
- import { tmpdir } from "node:os";
944
- import { homedir as homedir3, platform as platform4 } from "node:os";
945
- import { join as join5 } from "node:path";
946
- var PLATFORM2 = platform4();
947
- var IS_MACOS4 = PLATFORM2 === "darwin";
948
- var IS_LINUX2 = PLATFORM2 === "linux";
949
- function isMissingBunSqliteModule3(error) {
950
- if (!error || typeof error !== "object") {
951
- return false;
952
- }
953
- const err = error;
954
- const code = typeof err.code === "string" ? err.code : "";
955
- const message = typeof err.message === "string" ? err.message : "";
956
- if (code === "ERR_MODULE_NOT_FOUND" || code === "ERR_UNSUPPORTED_ESM_URL_SCHEME") {
957
- return true;
1077
+ import { existsSync as existsSync5 } from "node:fs";
1078
+ import { join as join6 } from "node:path";
1079
+ var CONTROL_CHAR_RE = /[\u0000-\u001F]/g;
1080
+ function toStringValue(value) {
1081
+ if (typeof value === "string") {
1082
+ return value;
958
1083
  }
959
- if (!message.includes("bun:sqlite")) {
960
- return false;
1084
+ if (value instanceof Uint8Array) {
1085
+ const buf = Buffer.from(value);
1086
+ const strings = [];
1087
+ strings.push(buf.toString("utf8"));
1088
+ strings.push(buf.toString("utf16le"));
1089
+ const [first] = buf;
1090
+ if (first === 0 || first === 1 || first === 2) {
1091
+ const sliced = buf.subarray(1);
1092
+ strings.push(sliced.toString("utf8"));
1093
+ strings.push(sliced.toString("utf16le"));
1094
+ }
1095
+ return strings.sort((a, b) => b.length - a.length)[0] ?? "";
961
1096
  }
962
- return message.includes("Cannot find module") || message.includes("Unknown builtin module") || message.includes("unsupported URL scheme") || message.includes("Only URLs with a scheme in");
963
- }
964
- async function queryReadonlySqlite3(dbPath, sql) {
965
- try {
966
- const { Database } = await import("bun:sqlite");
967
- const db = new Database(dbPath, { readonly: true });
968
- try {
969
- return db.query(sql).all();
970
- } finally {
971
- db.close();
972
- }
973
- } catch (error) {
974
- if (!isMissingBunSqliteModule3(error)) {
975
- throw error;
976
- }
977
- const { DatabaseSync } = await import("node:sqlite");
978
- const db = new DatabaseSync(dbPath, { readOnly: true });
979
- try {
980
- return db.prepare(sql).all();
981
- } finally {
982
- db.close();
983
- }
984
- }
985
- }
986
- function getFirefoxBaseDir() {
987
- if (IS_LINUX2) {
988
- return join5(homedir3(), ".mozilla", "firefox");
989
- }
990
- if (IS_MACOS4) {
991
- return join5(homedir3(), "Library", "Application Support", "Firefox");
992
- }
993
- throw new Error(`Firefox extraction is not supported on ${PLATFORM2}.`);
994
- }
995
- function parseProfilesIni(raw, baseDir) {
996
- const lines = raw.split(/\r?\n/);
997
- const profiles = [];
998
- const installDefaults = new Set;
999
- let section = "";
1000
- let current = null;
1001
- for (const lineRaw of lines) {
1002
- const line = lineRaw.trim();
1003
- if (!line || line.startsWith(";") || line.startsWith("#")) {
1004
- continue;
1005
- }
1006
- if (line.startsWith("[") && line.endsWith("]")) {
1007
- if (current) {
1008
- profiles.push(current);
1009
- current = null;
1010
- }
1011
- section = line.slice(1, -1);
1012
- if (section.startsWith("Profile")) {
1013
- current = { isRelative: true, isDefault: false };
1014
- }
1015
- continue;
1016
- }
1017
- const idx = line.indexOf("=");
1018
- if (idx === -1) {
1019
- continue;
1020
- }
1021
- const key = line.slice(0, idx).trim();
1022
- const value = line.slice(idx + 1).trim();
1023
- if (section.startsWith("Profile") && current) {
1024
- if (key === "Name") {
1025
- current.name = value;
1026
- } else if (key === "Path") {
1027
- current.path = value;
1028
- } else if (key === "IsRelative") {
1029
- current.isRelative = value !== "0";
1030
- } else if (key === "Default") {
1031
- current.isDefault = value === "1";
1032
- }
1033
- continue;
1034
- }
1035
- if (section.startsWith("Install") && key === "Default" && value) {
1036
- installDefaults.add(value);
1037
- }
1038
- }
1039
- if (current) {
1040
- profiles.push(current);
1041
- }
1042
- return profiles.filter((p) => Boolean(p.path)).map((p) => {
1043
- const profilePath = p.isRelative ? join5(baseDir, p.path) : p.path;
1044
- return {
1045
- name: p.name,
1046
- path: profilePath,
1047
- isDefault: p.isDefault || installDefaults.has(p.path)
1048
- };
1049
- });
1050
- }
1051
- async function listProfileCandidates() {
1052
- const baseDir = getFirefoxBaseDir();
1053
- const iniPath = join5(baseDir, "profiles.ini");
1054
- const candidates = [];
1055
- if (existsSync4(iniPath)) {
1056
- const raw = await readFile2(iniPath, "utf8");
1057
- candidates.push(...parseProfilesIni(raw, baseDir));
1058
- }
1059
- const dirName = IS_MACOS4 ? "Profiles" : baseDir;
1060
- if (existsSync4(dirName)) {
1061
- const entries = await readdir2(dirName, { withFileTypes: true });
1062
- for (const entry of entries) {
1063
- if (!entry.isDirectory()) {
1064
- continue;
1065
- }
1066
- const profilePath = join5(dirName, entry.name);
1067
- if (!candidates.some((c) => c.path === profilePath)) {
1068
- candidates.push({ path: profilePath, isDefault: false });
1069
- }
1070
- }
1071
- }
1072
- const existing = candidates.filter((c) => existsSync4(c.path));
1073
- existing.sort((a, b) => Number(b.isDefault) - Number(a.isDefault));
1074
- return existing;
1075
- }
1076
- function pickCandidatesByProfile(candidates, profile) {
1077
- const selector = profile?.trim();
1078
- if (!selector) {
1079
- return candidates;
1080
- }
1081
- const normalized = selector.toLowerCase();
1082
- const matched = candidates.filter((c) => {
1083
- const name = c.name?.toLowerCase() ?? "";
1084
- const base = c.path.split("/").pop()?.toLowerCase() ?? "";
1085
- const full = c.path.toLowerCase();
1086
- return name === normalized || base === normalized || full.includes(normalized);
1087
- });
1088
- return matched;
1089
- }
1090
- async function copySqliteForRead(dbPath) {
1091
- const tmpPath = await mkdtemp(join5(tmpdir(), "agent-slack-firefox-"));
1092
- const base = dbPath.split("/").pop() || "db.sqlite";
1093
- const copyPath = join5(tmpPath, base);
1094
- await copyFile(dbPath, copyPath);
1095
- for (const suffix of ["-wal", "-shm"]) {
1096
- const sidecar = `${dbPath}${suffix}`;
1097
- if (!existsSync4(sidecar)) {
1098
- continue;
1099
- }
1100
- try {
1101
- await copyFile(sidecar, `${copyPath}${suffix}`);
1102
- } catch {}
1103
- }
1104
- return {
1105
- copyPath,
1106
- cleanup: async () => {
1107
- try {
1108
- await rm2(tmpPath, { recursive: true, force: true });
1109
- } catch {}
1110
- }
1111
- };
1112
- }
1113
- function toStringValue(value) {
1114
- if (typeof value === "string") {
1115
- return value;
1116
- }
1117
- if (value instanceof Uint8Array) {
1118
- const buf = Buffer.from(value);
1119
- const strings = [];
1120
- strings.push(buf.toString("utf8"));
1121
- strings.push(buf.toString("utf16le"));
1122
- const first = buf[0];
1123
- if (first === 0 || first === 1 || first === 2) {
1124
- const sliced = buf.subarray(1);
1125
- strings.push(sliced.toString("utf8"));
1126
- strings.push(sliced.toString("utf16le"));
1127
- }
1128
- return strings.sort((a, b) => b.length - a.length)[0] ?? "";
1129
- }
1130
- return String(value ?? "");
1097
+ return String(value ?? "");
1131
1098
  }
1132
1099
  function parseJsonObjectFromValue(value) {
1133
1100
  const raw = toStringValue(value);
@@ -1147,7 +1114,7 @@ function parseJsonObjectFromValue(value) {
1147
1114
  if (direct) {
1148
1115
  return direct;
1149
1116
  }
1150
- const stripped = raw.replace(/[\u0000-\u001F]/g, "");
1117
+ const stripped = raw.replace(CONTROL_CHAR_RE, "");
1151
1118
  const strippedDirect = tryDecode(stripped);
1152
1119
  if (strippedDirect) {
1153
1120
  return strippedDirect;
@@ -1162,7 +1129,7 @@ function parseJsonObjectFromValue(value) {
1162
1129
  if (slicedDirect) {
1163
1130
  return slicedDirect;
1164
1131
  }
1165
- return tryDecode(sliced.replace(/[\u0000-\u001F]/g, ""));
1132
+ return tryDecode(sliced.replace(CONTROL_CHAR_RE, ""));
1166
1133
  }
1167
1134
  function toFirefoxTeam(value) {
1168
1135
  if (typeof value !== "object" || value === null) {
@@ -1182,9 +1149,7 @@ function extractTeamsFromRawText(raw) {
1182
1149
  const seen = new Set;
1183
1150
  const richPattern = /"name":"([^"]+)".*?"url":"(https:\/\/[^"\s]+slack\.com\/)".*?"token":"(xoxc-[^"]+)"/gs;
1184
1151
  for (const match of raw.matchAll(richPattern)) {
1185
- const name = match[1];
1186
- const url = match[2];
1187
- const token = match[3];
1152
+ const [, name, url, token] = match;
1188
1153
  if (!name || !url || !token) {
1189
1154
  continue;
1190
1155
  }
@@ -1217,26 +1182,26 @@ function extractTeamsFromRawText(raw) {
1217
1182
  return teams;
1218
1183
  }
1219
1184
  function getLocalStorageDirs(profilePath) {
1220
- const roots = [join5(profilePath, "storage", "default")];
1185
+ const roots = [join6(profilePath, "storage", "default")];
1221
1186
  const candidates = [];
1222
1187
  for (const root of roots) {
1223
- if (!existsSync4(root)) {
1188
+ if (!existsSync5(root)) {
1224
1189
  continue;
1225
1190
  }
1226
- candidates.push(join5(root, "https+++app.slack.com", "ls"));
1191
+ candidates.push(join6(root, "https+++app.slack.com", "ls"));
1227
1192
  }
1228
1193
  return candidates;
1229
1194
  }
1230
1195
  async function extractTeamsFromProfile(profilePath) {
1231
1196
  const lsDirs = getLocalStorageDirs(profilePath);
1232
1197
  for (const lsDir of lsDirs) {
1233
- const dbPath = join5(lsDir, "data.sqlite");
1234
- if (!existsSync4(dbPath)) {
1198
+ const dbPath = join6(lsDir, "data.sqlite");
1199
+ if (!existsSync5(dbPath)) {
1235
1200
  continue;
1236
1201
  }
1237
1202
  const copied = await copySqliteForRead(dbPath);
1238
1203
  try {
1239
- const rows = await queryReadonlySqlite3(copied.copyPath, "select key, value from data where key in ('localConfig_v2', 'localConfig_v3') order by key desc");
1204
+ const rows = await queryReadonlySqlite(copied.copyPath, "select key, value from data where key in ('localConfig_v2', 'localConfig_v3') order by key desc");
1240
1205
  for (const row of rows) {
1241
1206
  const cfg = parseJsonObjectFromValue(row.value);
1242
1207
  const teamsRaw = cfg && typeof cfg.teams === "object" && cfg.teams !== null ? cfg.teams : {};
@@ -1256,13 +1221,13 @@ async function extractTeamsFromProfile(profilePath) {
1256
1221
  return null;
1257
1222
  }
1258
1223
  async function extractCookieDFromProfile(profilePath) {
1259
- const dbPath = join5(profilePath, "cookies.sqlite");
1260
- if (!existsSync4(dbPath)) {
1224
+ const dbPath = join6(profilePath, "cookies.sqlite");
1225
+ if (!existsSync5(dbPath)) {
1261
1226
  return null;
1262
1227
  }
1263
1228
  const copied = await copySqliteForRead(dbPath);
1264
1229
  try {
1265
- const rows = await queryReadonlySqlite3(copied.copyPath, "select value from moz_cookies where host like '%slack.com%' and name='d' order by length(value) desc");
1230
+ const rows = await queryReadonlySqlite(copied.copyPath, "select value from moz_cookies where host like '%slack.com%' and name='d' order by length(value) desc");
1266
1231
  for (const row of rows) {
1267
1232
  if (row.value?.startsWith("xoxd-")) {
1268
1233
  return { cookie_d: decodeFirefoxCookieValue(row.value), sourcePath: dbPath };
@@ -1318,9 +1283,9 @@ async function extractFromFirefox(input) {
1318
1283
 
1319
1284
  // src/auth/paths.ts
1320
1285
  import { homedir as homedir4 } from "node:os";
1321
- import { join as join6 } from "node:path";
1322
- var AGENT_SLACK_DIR = join6(homedir4(), ".config", "agent-slack");
1323
- var CREDENTIALS_FILE = join6(AGENT_SLACK_DIR, "credentials.json");
1286
+ import { join as join7 } from "node:path";
1287
+ var AGENT_SLACK_DIR = join7(homedir4(), ".config", "agent-slack");
1288
+ var CREDENTIALS_FILE = join7(AGENT_SLACK_DIR, "credentials.json");
1324
1289
  var KEYCHAIN_SERVICE = "agent-slack";
1325
1290
 
1326
1291
  // src/lib/fs.ts
@@ -1575,50 +1540,6 @@ async function resolveDefaultWorkspace() {
1575
1540
  return creds.workspaces[0] ?? null;
1576
1541
  }
1577
1542
 
1578
- // src/cli/workspace-selector.ts
1579
- function normalizeUrl(u) {
1580
- const url = new URL(u);
1581
- return `${url.protocol}//${url.host}`;
1582
- }
1583
- function normalizedWorkspaceCandidates(workspace) {
1584
- let host = "";
1585
- try {
1586
- host = new URL(workspace.workspace_url).host.toLowerCase();
1587
- } catch {
1588
- host = "";
1589
- }
1590
- const hostWithoutSlackSuffix = host.replace(/\.slack\.com$/i, "");
1591
- return [
1592
- workspace.workspace_url.toLowerCase(),
1593
- host,
1594
- hostWithoutSlackSuffix,
1595
- workspace.workspace_name?.toLowerCase() ?? "",
1596
- workspace.team_domain?.toLowerCase() ?? ""
1597
- ].filter(Boolean);
1598
- }
1599
- function resolveWorkspaceSelector(workspaces, selector) {
1600
- const raw = selector.trim();
1601
- if (!raw) {
1602
- return { match: null, ambiguous: [] };
1603
- }
1604
- try {
1605
- const normalized = normalizeUrl(raw).toLowerCase();
1606
- const exact = workspaces.find((w) => w.workspace_url.toLowerCase() === normalized);
1607
- if (exact) {
1608
- return { match: exact, ambiguous: [] };
1609
- }
1610
- } catch {}
1611
- const needle = raw.toLowerCase();
1612
- const matches = workspaces.filter((workspace) => normalizedWorkspaceCandidates(workspace).some((candidate) => candidate.includes(needle)));
1613
- if (matches.length === 1) {
1614
- return { match: matches[0], ambiguous: [] };
1615
- }
1616
- if (matches.length > 1) {
1617
- return { match: null, ambiguous: matches };
1618
- }
1619
- return { match: null, ambiguous: [] };
1620
- }
1621
-
1622
1543
  // src/lib/object-type-guards.ts
1623
1544
  function isRecord5(value) {
1624
1545
  return typeof value === "object" && value !== null;
@@ -1764,6 +1685,50 @@ function normalizeConversationsLimit(value) {
1764
1685
  return Math.min(Math.max(value ?? 100, 1), 1000);
1765
1686
  }
1766
1687
 
1688
+ // src/cli/workspace-selector.ts
1689
+ function normalizeUrl(u) {
1690
+ const url = new URL(u);
1691
+ return `${url.protocol}//${url.host}`;
1692
+ }
1693
+ function normalizedWorkspaceCandidates(workspace) {
1694
+ let host = "";
1695
+ try {
1696
+ host = new URL(workspace.workspace_url).host.toLowerCase();
1697
+ } catch {
1698
+ host = "";
1699
+ }
1700
+ const hostWithoutSlackSuffix = host.replace(/\.slack\.com$/i, "");
1701
+ return [
1702
+ workspace.workspace_url.toLowerCase(),
1703
+ host,
1704
+ hostWithoutSlackSuffix,
1705
+ workspace.workspace_name?.toLowerCase() ?? "",
1706
+ workspace.team_domain?.toLowerCase() ?? ""
1707
+ ].filter(Boolean);
1708
+ }
1709
+ function resolveWorkspaceSelector(workspaces, selector) {
1710
+ const raw = selector.trim();
1711
+ if (!raw) {
1712
+ return { match: null, ambiguous: [] };
1713
+ }
1714
+ try {
1715
+ const normalized = normalizeUrl(raw).toLowerCase();
1716
+ const exact = workspaces.find((w) => w.workspace_url.toLowerCase() === normalized);
1717
+ if (exact) {
1718
+ return { match: exact, ambiguous: [] };
1719
+ }
1720
+ } catch {}
1721
+ const needle = raw.toLowerCase();
1722
+ const matches = workspaces.filter((workspace) => normalizedWorkspaceCandidates(workspace).some((candidate) => candidate.includes(needle)));
1723
+ if (matches.length === 1) {
1724
+ return { match: matches[0], ambiguous: [] };
1725
+ }
1726
+ if (matches.length > 1) {
1727
+ return { match: null, ambiguous: matches };
1728
+ }
1729
+ return { match: null, ambiguous: [] };
1730
+ }
1731
+
1767
1732
  // src/slack/client.ts
1768
1733
  import { WebClient } from "@slack/web-api";
1769
1734
  class SlackApiClient {
@@ -1801,7 +1766,7 @@ class SlackApiClient {
1801
1766
  async browserApi(input) {
1802
1767
  const attempt = input.attempt ?? 0;
1803
1768
  const url = `${input.workspaceUrl.replace(/\/$/, "")}/api/${input.method}`;
1804
- const cleanedEntries = Object.entries(input.params).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)]);
1769
+ const cleanedEntries = Object.entries(input.params).filter(([, v]) => v !== undefined).map(([k, v]) => [k, typeof v === "object" ? JSON.stringify(v) : String(v)]);
1805
1770
  const formBody = new URLSearchParams({
1806
1771
  token: input.auth.xoxc_token,
1807
1772
  ...Object.fromEntries(cleanedEntries)
@@ -1840,86 +1805,16 @@ function isRecord6(value) {
1840
1805
  return typeof value === "object" && value !== null;
1841
1806
  }
1842
1807
 
1843
- // src/cli/context.ts
1844
- function isEnvAuthConfigured() {
1845
- return Boolean(process.env.SLACK_TOKEN?.trim());
1846
- }
1847
- function effectiveWorkspaceUrl(flag) {
1848
- return flag?.trim() || process.env.SLACK_WORKSPACE_URL?.trim() || undefined;
1849
- }
1850
- function errorMessage(err) {
1851
- return err instanceof Error ? err.message : String(err);
1808
+ // src/cli/context-client-resolver.ts
1809
+ function normalizeUrl2(u) {
1810
+ const url = new URL(u);
1811
+ return `${url.protocol}//${url.host}`;
1852
1812
  }
1853
- function parseContentType(value) {
1854
- const raw = String(value ?? "any").toLowerCase();
1855
- if (raw === "text" || raw === "image" || raw === "snippet" || raw === "file") {
1856
- return raw;
1857
- }
1858
- return "any";
1859
- }
1860
- async function assertWorkspaceSpecifiedForChannelNames(input) {
1861
- const hasName = input.channels.some((c) => normalizeChannelInput(c).kind === "name");
1862
- if (!hasName) {
1863
- return;
1864
- }
1865
- const creds = await loadCredentials();
1866
- if ((creds.workspaces?.length ?? 0) <= 1) {
1867
- return;
1868
- }
1869
- if (!input.workspaceUrl) {
1870
- throw new Error('Ambiguous channel name across multiple workspaces. Pass --workspace "<url-or-unique-substring>" (or set SLACK_WORKSPACE_URL).');
1871
- }
1872
- }
1873
- function isAuthErrorMessage(message) {
1874
- return /(?:^|[^a-z])(invalid_auth|token_expired)(?:$|[^a-z])/i.test(message);
1875
- }
1876
- function normalizeUrl2(u) {
1877
- const url = new URL(u);
1878
- return `${url.protocol}//${url.host}`;
1879
- }
1880
- function tryNormalizeUrl(u) {
1881
- try {
1882
- return normalizeUrl2(u);
1883
- } catch {
1884
- return;
1885
- }
1886
- }
1887
- async function refreshFromDesktopIfPossible() {
1888
- if (process.platform !== "darwin" && process.platform !== "linux") {
1889
- return false;
1890
- }
1891
- try {
1892
- const extracted = await extractFromSlackDesktop();
1893
- await upsertWorkspaces(extracted.teams.map((team) => ({
1894
- workspace_url: normalizeUrl2(team.url),
1895
- workspace_name: team.name,
1896
- auth: {
1897
- auth_type: "browser",
1898
- xoxc_token: team.token,
1899
- xoxd_cookie: extracted.cookie_d
1900
- }
1901
- })));
1902
- return true;
1903
- } catch {
1904
- return false;
1905
- }
1906
- }
1907
- async function withAutoRefresh(input) {
1908
- try {
1909
- return await input.work();
1910
- } catch (err) {
1911
- const message = errorMessage(err);
1912
- if (isEnvAuthConfigured()) {
1913
- throw err;
1914
- }
1915
- if (!isAuthErrorMessage(message)) {
1916
- throw err;
1917
- }
1918
- const refreshed = await refreshFromDesktopIfPossible();
1919
- if (!refreshed) {
1920
- throw err;
1921
- }
1922
- return await input.work();
1813
+ function tryNormalizeUrl(u) {
1814
+ try {
1815
+ return normalizeUrl2(u);
1816
+ } catch {
1817
+ return;
1923
1818
  }
1924
1819
  }
1925
1820
  function pickAuthFromEnv() {
@@ -2011,104 +1906,161 @@ async function getClientForWorkspace(workspaceUrl) {
2011
1906
  };
2012
1907
  }
2013
1908
  } catch {}
2014
- const chrome = extractFromChrome() ?? await extractFromBrave();
2015
- if (chrome && chrome.teams.length > 0) {
2016
- let chosen = chrome.teams[0];
2017
- if (selector) {
2018
- const normalizedSelector = selector.toLowerCase();
2019
- const matches = chrome.teams.filter((t) => {
2020
- const normalizedUrl = normalizeUrl2(t.url).toLowerCase();
2021
- const host = new URL(t.url).host.toLowerCase();
2022
- const hostWithoutSlackSuffix = host.replace(/\.slack\.com$/i, "");
2023
- const name = t.name?.toLowerCase() ?? "";
2024
- return normalizedUrl.includes(normalizedSelector) || host.includes(normalizedSelector) || hostWithoutSlackSuffix.includes(normalizedSelector) || name.includes(normalizedSelector);
2025
- });
2026
- if (matches.length > 1) {
2027
- 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.`);
2028
- }
2029
- if (matches.length === 1) {
2030
- chosen = matches[0];
2031
- } else if (normalizedSelectorUrl) {
2032
- try {
2033
- chosen = chrome.teams.find((t) => normalizeUrl2(t.url) === normalizeUrl2(selector)) ?? chosen;
2034
- } catch {}
2035
- } else {
2036
- throw new Error(`No configured workspace matches selector "${selector}". Run "agent-slack auth whoami" to list available workspaces.`);
2037
- }
2038
- }
2039
- const auth = {
2040
- auth_type: "browser",
2041
- xoxc_token: chosen.token,
2042
- xoxd_cookie: chrome.cookie_d
2043
- };
2044
- await upsertWorkspace({
2045
- workspace_url: normalizeUrl2(chosen.url),
2046
- workspace_name: chosen.name,
2047
- auth: {
2048
- auth_type: "browser",
2049
- xoxc_token: chosen.token,
2050
- xoxd_cookie: chrome.cookie_d
2051
- }
1909
+ const browserSources = [];
1910
+ const chromeResult = extractFromChrome();
1911
+ if (chromeResult && chromeResult.teams.length > 0) {
1912
+ browserSources.push(chromeResult);
1913
+ }
1914
+ const braveResult = await extractFromBrave();
1915
+ if (braveResult && braveResult.teams.length > 0) {
1916
+ browserSources.push(braveResult);
1917
+ }
1918
+ const firefoxResult = await extractFromFirefox();
1919
+ if (firefoxResult && firefoxResult.teams.length > 0) {
1920
+ browserSources.push(firefoxResult);
1921
+ }
1922
+ for (const source of browserSources) {
1923
+ const result = await matchAndUpsertBrowserTeam({
1924
+ teams: source.teams,
1925
+ cookieD: source.cookie_d,
1926
+ selector,
1927
+ normalizedSelectorUrl
2052
1928
  });
2053
- return {
2054
- client: new SlackApiClient(auth, {
2055
- workspaceUrl: normalizeUrl2(chosen.url)
2056
- }),
2057
- auth,
2058
- workspace_url: normalizeUrl2(chosen.url)
2059
- };
1929
+ if (result) {
1930
+ return result;
1931
+ }
2060
1932
  }
2061
- const firefox = await extractFromFirefox();
2062
- if (firefox && firefox.teams.length > 0) {
2063
- let chosen = firefox.teams[0];
2064
- if (selector) {
2065
- const normalizedSelector = selector.toLowerCase();
2066
- const matches = firefox.teams.filter((t) => {
2067
- const normalizedUrl = normalizeUrl2(t.url).toLowerCase();
2068
- const host = new URL(t.url).host.toLowerCase();
2069
- const hostWithoutSlackSuffix = host.replace(/\.slack\.com$/i, "");
2070
- const name = t.name?.toLowerCase() ?? "";
2071
- return normalizedUrl.includes(normalizedSelector) || host.includes(normalizedSelector) || hostWithoutSlackSuffix.includes(normalizedSelector) || name.includes(normalizedSelector);
2072
- });
2073
- if (matches.length > 1) {
2074
- throw new Error(`Workspace selector "${selector}" is ambiguous in Firefox workspaces. Matches: ${matches.map((t) => normalizeUrl2(t.url)).join(", ")}. Pass a more specific selector or full workspace URL.`);
2075
- }
2076
- if (matches.length === 1) {
2077
- chosen = matches[0];
2078
- } else if (normalizedSelectorUrl) {
1933
+ if (selector && !normalizedSelectorUrl) {
1934
+ throw new Error(`No configured workspace matches selector "${selector}". Run "agent-slack auth whoami" to list available workspaces.`);
1935
+ }
1936
+ throw new Error('No Slack credentials available. Try "agent-slack auth import-desktop", "agent-slack auth import-chrome", "agent-slack auth import-brave", "agent-slack auth import-firefox", or set SLACK_TOKEN / SLACK_COOKIE_D.');
1937
+ }
1938
+ async function matchAndUpsertBrowserTeam(input) {
1939
+ const { teams, cookieD, selector, normalizedSelectorUrl } = input;
1940
+ let chosen = teams[0];
1941
+ if (selector) {
1942
+ const normalizedSelector = selector.toLowerCase();
1943
+ const matches = teams.filter((t) => {
1944
+ const normalizedUrl = normalizeUrl2(t.url).toLowerCase();
1945
+ const host = new URL(t.url).host.toLowerCase();
1946
+ const hostWithoutSlackSuffix = host.replace(/\.slack\.com$/i, "");
1947
+ const name = t.name?.toLowerCase() ?? "";
1948
+ return normalizedUrl.includes(normalizedSelector) || host.includes(normalizedSelector) || hostWithoutSlackSuffix.includes(normalizedSelector) || name.includes(normalizedSelector);
1949
+ });
1950
+ if (matches.length > 1) {
1951
+ throw new Error(`Workspace selector "${selector}" is ambiguous. Matches: ${matches.map((t) => normalizeUrl2(t.url)).join(", ")}. Pass a more specific selector or full workspace URL.`);
1952
+ }
1953
+ if (matches.length === 1) {
1954
+ chosen = matches[0];
1955
+ } else if (normalizedSelectorUrl) {
1956
+ const exact = teams.find((t) => {
2079
1957
  try {
2080
- chosen = firefox.teams.find((t) => normalizeUrl2(t.url) === normalizeUrl2(selector)) ?? chosen;
2081
- } catch {}
1958
+ return normalizeUrl2(t.url) === normalizeUrl2(selector);
1959
+ } catch {
1960
+ return false;
1961
+ }
1962
+ });
1963
+ if (exact) {
1964
+ chosen = exact;
2082
1965
  } else {
2083
- throw new Error(`No configured workspace matches selector "${selector}". Run "agent-slack auth whoami" to list available workspaces.`);
1966
+ return null;
2084
1967
  }
1968
+ } else {
1969
+ return null;
2085
1970
  }
2086
- const auth = {
1971
+ }
1972
+ const auth = {
1973
+ auth_type: "browser",
1974
+ xoxc_token: chosen.token,
1975
+ xoxd_cookie: cookieD
1976
+ };
1977
+ const workspaceUrl = normalizeUrl2(chosen.url);
1978
+ await upsertWorkspace({
1979
+ workspace_url: workspaceUrl,
1980
+ workspace_name: chosen.name,
1981
+ auth: {
2087
1982
  auth_type: "browser",
2088
1983
  xoxc_token: chosen.token,
2089
- xoxd_cookie: firefox.cookie_d
2090
- };
2091
- await upsertWorkspace({
2092
- workspace_url: normalizeUrl2(chosen.url),
2093
- workspace_name: chosen.name,
1984
+ xoxd_cookie: cookieD
1985
+ }
1986
+ });
1987
+ return {
1988
+ client: new SlackApiClient(auth, { workspaceUrl }),
1989
+ auth,
1990
+ workspace_url: workspaceUrl
1991
+ };
1992
+ }
1993
+
1994
+ // src/cli/context.ts
1995
+ function isEnvAuthConfigured() {
1996
+ return Boolean(process.env.SLACK_TOKEN?.trim());
1997
+ }
1998
+ function effectiveWorkspaceUrl(flag) {
1999
+ return flag?.trim() || process.env.SLACK_WORKSPACE_URL?.trim() || undefined;
2000
+ }
2001
+ function errorMessage(err) {
2002
+ return err instanceof Error ? err.message : String(err);
2003
+ }
2004
+ function parseContentType(value) {
2005
+ const raw = String(value ?? "any").toLowerCase();
2006
+ if (raw === "text" || raw === "image" || raw === "snippet" || raw === "file") {
2007
+ return raw;
2008
+ }
2009
+ return "any";
2010
+ }
2011
+ async function assertWorkspaceSpecifiedForChannelNames(input) {
2012
+ const hasName = input.channels.some((c) => normalizeChannelInput(c).kind === "name");
2013
+ if (!hasName) {
2014
+ return;
2015
+ }
2016
+ const creds = await loadCredentials();
2017
+ if ((creds.workspaces?.length ?? 0) <= 1) {
2018
+ return;
2019
+ }
2020
+ if (!input.workspaceUrl) {
2021
+ throw new Error('Ambiguous channel name across multiple workspaces. Pass --workspace "<url-or-unique-substring>" (or set SLACK_WORKSPACE_URL).');
2022
+ }
2023
+ }
2024
+ function isAuthErrorMessage(message) {
2025
+ return /(?:^|[^a-z])(invalid_auth|token_expired)(?:$|[^a-z])/i.test(message);
2026
+ }
2027
+ async function refreshFromDesktopIfPossible() {
2028
+ if (process.platform !== "darwin" && process.platform !== "linux") {
2029
+ return false;
2030
+ }
2031
+ try {
2032
+ const extracted = await extractFromSlackDesktop();
2033
+ await upsertWorkspaces(extracted.teams.map((team) => ({
2034
+ workspace_url: normalizeUrl2(team.url),
2035
+ workspace_name: team.name,
2094
2036
  auth: {
2095
2037
  auth_type: "browser",
2096
- xoxc_token: chosen.token,
2097
- xoxd_cookie: firefox.cookie_d
2038
+ xoxc_token: team.token,
2039
+ xoxd_cookie: extracted.cookie_d
2098
2040
  }
2099
- });
2100
- return {
2101
- client: new SlackApiClient(auth, {
2102
- workspaceUrl: normalizeUrl2(chosen.url)
2103
- }),
2104
- auth,
2105
- workspace_url: normalizeUrl2(chosen.url)
2106
- };
2041
+ })));
2042
+ return true;
2043
+ } catch {
2044
+ return false;
2107
2045
  }
2108
- if (selector && !normalizedSelectorUrl) {
2109
- throw new Error(`No configured workspace matches selector "${selector}". Run "agent-slack auth whoami" to list available workspaces.`);
2046
+ }
2047
+ async function withAutoRefresh(input) {
2048
+ try {
2049
+ return await input.work();
2050
+ } catch (err) {
2051
+ const message = errorMessage(err);
2052
+ if (isEnvAuthConfigured()) {
2053
+ throw err;
2054
+ }
2055
+ if (!isAuthErrorMessage(message)) {
2056
+ throw err;
2057
+ }
2058
+ const refreshed = await refreshFromDesktopIfPossible();
2059
+ if (!refreshed) {
2060
+ throw err;
2061
+ }
2062
+ return await input.work();
2110
2063
  }
2111
- throw new Error('No Slack credentials available. Try "agent-slack auth import-desktop", "agent-slack auth import-chrome", "agent-slack auth import-brave", "agent-slack auth import-firefox", or set SLACK_TOKEN / SLACK_COOKIE_D.');
2112
2064
  }
2113
2065
  function createCliContext() {
2114
2066
  return {
@@ -2386,15 +2338,15 @@ function registerAuthCommand(input) {
2386
2338
 
2387
2339
  // src/slack/files.ts
2388
2340
  import { mkdir as mkdir3, writeFile as writeFile2 } from "node:fs/promises";
2389
- import { basename, join as join7, resolve } from "node:path";
2390
- import { existsSync as existsSync5 } from "node:fs";
2341
+ import { basename, join as join8, resolve } from "node:path";
2342
+ import { existsSync as existsSync6 } from "node:fs";
2391
2343
  async function downloadSlackFile(input) {
2392
2344
  const { auth, url, destDir, preferredName, options } = input;
2393
2345
  const absDir = resolve(destDir);
2394
2346
  await mkdir3(absDir, { recursive: true });
2395
2347
  const name = sanitizeFilename(preferredName || basename(new URL(url).pathname) || "file");
2396
- const path = join7(absDir, name);
2397
- if (existsSync5(path)) {
2348
+ const path = join8(absDir, name);
2349
+ if (existsSync6(path)) {
2398
2350
  return path;
2399
2351
  }
2400
2352
  const headers = {};
@@ -2449,27 +2401,27 @@ function extractTag(html, tag) {
2449
2401
  }
2450
2402
 
2451
2403
  // src/lib/tmp-paths.ts
2452
- import { join as join9, resolve as resolve2 } from "node:path";
2404
+ import { join as join10, resolve as resolve2 } from "node:path";
2453
2405
  import { mkdir as mkdir4 } from "node:fs/promises";
2454
2406
 
2455
2407
  // src/lib/app-dir.ts
2456
2408
  import { homedir as homedir5, tmpdir as tmpdir2 } from "node:os";
2457
- import { join as join8 } from "node:path";
2409
+ import { join as join9 } from "node:path";
2458
2410
  function getAppDir() {
2459
2411
  const xdg = process.env.XDG_RUNTIME_DIR?.trim();
2460
2412
  if (xdg) {
2461
- return join8(xdg, "agent-slack");
2413
+ return join9(xdg, "agent-slack");
2462
2414
  }
2463
2415
  const home = homedir5();
2464
2416
  if (home) {
2465
- return join8(home, ".agent-slack");
2417
+ return join9(home, ".agent-slack");
2466
2418
  }
2467
- return join8(tmpdir2(), "agent-slack");
2419
+ return join9(tmpdir2(), "agent-slack");
2468
2420
  }
2469
2421
 
2470
2422
  // src/lib/tmp-paths.ts
2471
2423
  function getDownloadsDir() {
2472
- return resolve2(join9(getAppDir(), "tmp", "downloads"));
2424
+ return resolve2(join10(getAppDir(), "tmp", "downloads"));
2473
2425
  }
2474
2426
  async function ensureDownloadsDir() {
2475
2427
  const dir = getDownloadsDir();
@@ -2634,119 +2586,70 @@ function slackMrkdwnToMarkdown(text) {
2634
2586
  return out;
2635
2587
  }
2636
2588
 
2637
- // src/slack/render.ts
2638
- var MAX_ATTACHMENT_DEPTH = 8;
2639
- function renderSlackMessageContent(msg) {
2640
- const msgObj = isRecord7(msg) ? msg : {};
2641
- const blockMrkdwn = extractMrkdwnFromBlocks(msgObj.blocks);
2642
- const attachmentMrkdwn = extractMrkdwnFromAttachments(msgObj.attachments, {
2643
- depth: 0,
2644
- seen: new WeakSet
2645
- });
2646
- const combined = [blockMrkdwn.trim(), attachmentMrkdwn.trim()].filter(Boolean).join(`
2647
-
2648
- `);
2649
- if (combined) {
2650
- return slackMrkdwnToMarkdown(combined).trim();
2589
+ // src/slack/message-api-parsing.ts
2590
+ function toSlackFileSummary(value) {
2591
+ if (!isRecord5(value)) {
2592
+ return null;
2651
2593
  }
2652
- const text = getString2(msgObj.text).trim();
2653
- if (text) {
2654
- return slackMrkdwnToMarkdown(text).trim();
2594
+ const id = getString(value.id);
2595
+ if (!id) {
2596
+ return null;
2655
2597
  }
2656
- return "";
2598
+ return {
2599
+ id,
2600
+ name: getString(value.name),
2601
+ title: getString(value.title),
2602
+ mimetype: getString(value.mimetype),
2603
+ filetype: getString(value.filetype),
2604
+ mode: getString(value.mode),
2605
+ permalink: getString(value.permalink),
2606
+ url_private: getString(value.url_private),
2607
+ url_private_download: getString(value.url_private_download),
2608
+ size: getNumber(value.size)
2609
+ };
2657
2610
  }
2658
- function extractMrkdwnFromBlocks(blocks) {
2659
- if (!Array.isArray(blocks)) {
2611
+ async function enrichFiles(client, files) {
2612
+ const out = [];
2613
+ for (const f of files) {
2614
+ if (f.mode === "snippet" || !f.url_private_download) {
2615
+ try {
2616
+ const info = await client.api("files.info", { file: f.id });
2617
+ const file = isRecord5(info.file) ? info.file : null;
2618
+ out.push({
2619
+ ...f,
2620
+ name: f.name ?? getString(file?.name),
2621
+ title: f.title ?? getString(file?.title),
2622
+ mimetype: f.mimetype ?? getString(file?.mimetype),
2623
+ filetype: f.filetype ?? getString(file?.filetype),
2624
+ mode: f.mode ?? getString(file?.mode),
2625
+ permalink: f.permalink ?? getString(file?.permalink),
2626
+ url_private: f.url_private ?? getString(file?.url_private),
2627
+ url_private_download: f.url_private_download ?? getString(file?.url_private_download),
2628
+ snippet: {
2629
+ content: getString(file?.content),
2630
+ language: getString(file?.filetype)
2631
+ }
2632
+ });
2633
+ continue;
2634
+ } catch {}
2635
+ }
2636
+ out.push(f);
2637
+ }
2638
+ return out;
2639
+ }
2640
+
2641
+ // src/slack/render-rich-text.ts
2642
+ function isRecord7(value) {
2643
+ return typeof value === "object" && value !== null;
2644
+ }
2645
+ function getString2(value) {
2646
+ return typeof value === "string" ? value : "";
2647
+ }
2648
+ function extractMrkdwnFromRichTextBlock(block) {
2649
+ if (!isRecord7(block)) {
2660
2650
  return "";
2661
2651
  }
2662
- const out = [];
2663
- for (const b of blocks) {
2664
- if (!isRecord7(b)) {
2665
- continue;
2666
- }
2667
- const type = getString2(b.type);
2668
- if (type === "section") {
2669
- const text = isRecord7(b.text) ? b.text : null;
2670
- const textType = text ? getString2(text.type) : "";
2671
- if (textType === "mrkdwn" || textType === "plain_text") {
2672
- out.push(getString2(text?.text));
2673
- }
2674
- if (Array.isArray(b.fields)) {
2675
- for (const f of b.fields) {
2676
- if (!isRecord7(f)) {
2677
- continue;
2678
- }
2679
- const fieldType = getString2(f.type);
2680
- if (fieldType === "mrkdwn" || fieldType === "plain_text") {
2681
- out.push(getString2(f.text));
2682
- }
2683
- }
2684
- }
2685
- const accessory = isRecord7(b.accessory) ? b.accessory : null;
2686
- if (getString2(accessory?.type) === "button") {
2687
- const label = getString2(accessory?.text?.text);
2688
- const url = getString2(accessory?.url);
2689
- if (url) {
2690
- out.push(label ? `${label}: ${url}` : url);
2691
- }
2692
- }
2693
- continue;
2694
- }
2695
- if (type === "actions" && Array.isArray(b.elements)) {
2696
- for (const el of b.elements) {
2697
- if (!isRecord7(el)) {
2698
- continue;
2699
- }
2700
- if (getString2(el.type) === "button") {
2701
- const label = getString2(el.text?.text);
2702
- const url = getString2(el.url);
2703
- if (url) {
2704
- out.push(label ? `${label}: ${url}` : url);
2705
- }
2706
- }
2707
- }
2708
- continue;
2709
- }
2710
- if (type === "context" && Array.isArray(b.elements)) {
2711
- for (const el of b.elements) {
2712
- if (!isRecord7(el)) {
2713
- continue;
2714
- }
2715
- const elType = getString2(el.type);
2716
- if (elType === "mrkdwn") {
2717
- out.push(getString2(el.text));
2718
- }
2719
- if (elType === "plain_text") {
2720
- out.push(getString2(el.text));
2721
- }
2722
- }
2723
- continue;
2724
- }
2725
- if (type === "image") {
2726
- const alt = getString2(b.alt_text);
2727
- const url = getString2(b.image_url);
2728
- if (url) {
2729
- out.push(alt ? `${alt}: ${url}` : url);
2730
- }
2731
- continue;
2732
- }
2733
- if (type === "rich_text") {
2734
- const rich = extractMrkdwnFromRichTextBlock(b);
2735
- if (rich.trim()) {
2736
- out.push(rich);
2737
- }
2738
- continue;
2739
- }
2740
- }
2741
- return out.join(`
2742
-
2743
- `);
2744
- }
2745
- function extractMrkdwnFromRichTextBlock(block) {
2746
- if (!isRecord7(block)) {
2747
- return "";
2748
- }
2749
- const elements = Array.isArray(block.elements) ? block.elements : [];
2652
+ const elements = Array.isArray(block.elements) ? block.elements : [];
2750
2653
  const out = [];
2751
2654
  for (const el of elements) {
2752
2655
  const txt = extractMrkdwnFromRichTextElement(el);
@@ -2795,13 +2698,14 @@ function extractMrkdwnFromRichTextElement(el) {
2795
2698
  const style = typeof el.style === "string" ? el.style : "bullet";
2796
2699
  const items = [];
2797
2700
  const itemEls = Array.isArray(el.elements) ? el.elements : [];
2798
- for (let idx = 0;idx < itemEls.length; idx++) {
2799
- const item = itemEls[idx];
2701
+ let num = 0;
2702
+ for (const item of itemEls) {
2800
2703
  const txt = extractMrkdwnFromRichTextElement(item).trim();
2801
2704
  if (!txt) {
2802
2705
  continue;
2803
2706
  }
2804
- const prefix = style === "ordered" ? `${idx + 1}. ` : "- ";
2707
+ num++;
2708
+ const prefix = style === "ordered" ? `${num}. ` : "- ";
2805
2709
  items.push(`${prefix}${txt}`);
2806
2710
  }
2807
2711
  return items.join(`
@@ -2850,6 +2754,115 @@ function extractMrkdwnFromRichTextElement(el) {
2850
2754
  }
2851
2755
  return "";
2852
2756
  }
2757
+
2758
+ // src/slack/render.ts
2759
+ var MAX_ATTACHMENT_DEPTH = 8;
2760
+ function renderSlackMessageContent(msg) {
2761
+ const msgObj = isRecord8(msg) ? msg : {};
2762
+ const blockMrkdwn = extractMrkdwnFromBlocks(msgObj.blocks);
2763
+ const attachmentMrkdwn = extractMrkdwnFromAttachments(msgObj.attachments, {
2764
+ depth: 0,
2765
+ seen: new WeakSet
2766
+ });
2767
+ const combined = [blockMrkdwn.trim(), attachmentMrkdwn.trim()].filter(Boolean).join(`
2768
+
2769
+ `);
2770
+ if (combined) {
2771
+ return slackMrkdwnToMarkdown(combined).trim();
2772
+ }
2773
+ const text = getString3(msgObj.text).trim();
2774
+ if (text) {
2775
+ return slackMrkdwnToMarkdown(text).trim();
2776
+ }
2777
+ return "";
2778
+ }
2779
+ function extractMrkdwnFromBlocks(blocks) {
2780
+ if (!Array.isArray(blocks)) {
2781
+ return "";
2782
+ }
2783
+ const out = [];
2784
+ for (const b of blocks) {
2785
+ if (!isRecord8(b)) {
2786
+ continue;
2787
+ }
2788
+ const type = getString3(b.type);
2789
+ if (type === "section") {
2790
+ const text = isRecord8(b.text) ? b.text : null;
2791
+ const textType = text ? getString3(text.type) : "";
2792
+ if (textType === "mrkdwn" || textType === "plain_text") {
2793
+ out.push(getString3(text?.text));
2794
+ }
2795
+ if (Array.isArray(b.fields)) {
2796
+ for (const f of b.fields) {
2797
+ if (!isRecord8(f)) {
2798
+ continue;
2799
+ }
2800
+ const fieldType = getString3(f.type);
2801
+ if (fieldType === "mrkdwn" || fieldType === "plain_text") {
2802
+ out.push(getString3(f.text));
2803
+ }
2804
+ }
2805
+ }
2806
+ const accessory = isRecord8(b.accessory) ? b.accessory : null;
2807
+ if (getString3(accessory?.type) === "button") {
2808
+ const label = getString3(accessory?.text?.text);
2809
+ const url = getString3(accessory?.url);
2810
+ if (url) {
2811
+ out.push(label ? `${label}: ${url}` : url);
2812
+ }
2813
+ }
2814
+ continue;
2815
+ }
2816
+ if (type === "actions" && Array.isArray(b.elements)) {
2817
+ for (const el of b.elements) {
2818
+ if (!isRecord8(el)) {
2819
+ continue;
2820
+ }
2821
+ if (getString3(el.type) === "button") {
2822
+ const label = getString3(el.text?.text);
2823
+ const url = getString3(el.url);
2824
+ if (url) {
2825
+ out.push(label ? `${label}: ${url}` : url);
2826
+ }
2827
+ }
2828
+ }
2829
+ continue;
2830
+ }
2831
+ if (type === "context" && Array.isArray(b.elements)) {
2832
+ for (const el of b.elements) {
2833
+ if (!isRecord8(el)) {
2834
+ continue;
2835
+ }
2836
+ const elType = getString3(el.type);
2837
+ if (elType === "mrkdwn") {
2838
+ out.push(getString3(el.text));
2839
+ }
2840
+ if (elType === "plain_text") {
2841
+ out.push(getString3(el.text));
2842
+ }
2843
+ }
2844
+ continue;
2845
+ }
2846
+ if (type === "image") {
2847
+ const alt = getString3(b.alt_text);
2848
+ const url = getString3(b.image_url);
2849
+ if (url) {
2850
+ out.push(alt ? `${alt}: ${url}` : url);
2851
+ }
2852
+ continue;
2853
+ }
2854
+ if (type === "rich_text") {
2855
+ const rich = extractMrkdwnFromRichTextBlock(b);
2856
+ if (rich.trim()) {
2857
+ out.push(rich);
2858
+ }
2859
+ continue;
2860
+ }
2861
+ }
2862
+ return out.join(`
2863
+
2864
+ `);
2865
+ }
2853
2866
  function extractMrkdwnFromAttachments(attachments, state) {
2854
2867
  if (state.depth >= MAX_ATTACHMENT_DEPTH) {
2855
2868
  return "";
@@ -2859,7 +2872,7 @@ function extractMrkdwnFromAttachments(attachments, state) {
2859
2872
  }
2860
2873
  const parts = [];
2861
2874
  for (const a of attachments) {
2862
- if (!isRecord7(a)) {
2875
+ if (!isRecord8(a)) {
2863
2876
  continue;
2864
2877
  }
2865
2878
  if (state.seen.has(a)) {
@@ -2870,7 +2883,7 @@ function extractMrkdwnFromAttachments(attachments, state) {
2870
2883
  const chunk = [];
2871
2884
  if (isSharedMessage) {
2872
2885
  chunk.push(formatForwardHeader(a));
2873
- const body = extractForwardedMessageBody(a, state).trim() || extractMrkdwnFromAttachments(a.attachments, nextState(state)).trim() || getString2(a.text).trim();
2886
+ const body = extractForwardedMessageBody(a, state).trim() || extractMrkdwnFromAttachments(a.attachments, nextState(state)).trim() || getString3(a.text).trim();
2874
2887
  if (body) {
2875
2888
  chunk.push(quoteMarkdown(body));
2876
2889
  }
@@ -2884,12 +2897,12 @@ function extractMrkdwnFromAttachments(attachments, state) {
2884
2897
  if (blocks.trim()) {
2885
2898
  chunk.push(blocks);
2886
2899
  }
2887
- const pretext = getString2(a.pretext);
2900
+ const pretext = getString3(a.pretext);
2888
2901
  if (pretext) {
2889
2902
  chunk.push(pretext);
2890
2903
  }
2891
- const title = getString2(a.title);
2892
- const titleLink = getString2(a.title_link);
2904
+ const title = getString3(a.title);
2905
+ const titleLink = getString3(a.title_link);
2893
2906
  if (titleLink && title) {
2894
2907
  chunk.push(`<${titleLink}|${title}>`);
2895
2908
  } else if (title) {
@@ -2897,17 +2910,17 @@ function extractMrkdwnFromAttachments(attachments, state) {
2897
2910
  } else if (titleLink) {
2898
2911
  chunk.push(titleLink);
2899
2912
  }
2900
- const text = getString2(a.text);
2913
+ const text = getString3(a.text);
2901
2914
  if (text) {
2902
2915
  chunk.push(text);
2903
2916
  }
2904
2917
  if (Array.isArray(a.fields)) {
2905
2918
  for (const f of a.fields) {
2906
- if (!isRecord7(f)) {
2919
+ if (!isRecord8(f)) {
2907
2920
  continue;
2908
2921
  }
2909
- const fieldTitle = getString2(f.title);
2910
- const value = getString2(f.value);
2922
+ const fieldTitle = getString3(f.title);
2923
+ const value = getString3(f.value);
2911
2924
  if (fieldTitle && value) {
2912
2925
  chunk.push(`${fieldTitle}
2913
2926
  ${value}`);
@@ -2918,7 +2931,7 @@ ${value}`);
2918
2931
  }
2919
2932
  }
2920
2933
  }
2921
- const fallback = getString2(a.fallback);
2934
+ const fallback = getString3(a.fallback);
2922
2935
  if (chunk.length === 0 && fallback) {
2923
2936
  chunk.push(fallback);
2924
2937
  }
@@ -2936,9 +2949,9 @@ ${value}`);
2936
2949
  `);
2937
2950
  }
2938
2951
  function formatForwardHeader(a) {
2939
- const authorName = getString2(a.author_name);
2940
- const authorLink = getString2(a.author_link);
2941
- const fromUrl = getString2(a.from_url);
2952
+ const authorName = getString3(a.author_name);
2953
+ const authorLink = getString3(a.author_link);
2954
+ const fromUrl = getString3(a.from_url);
2942
2955
  const authorPart = authorName && authorLink ? `<${authorLink}|${authorName}>` : authorName || "";
2943
2956
  const sourcePart = fromUrl ? `<${fromUrl}|original>` : "";
2944
2957
  if (authorPart && sourcePart) {
@@ -2960,14 +2973,14 @@ function extractForwardedMessageBody(attachment, state) {
2960
2973
  }
2961
2974
  const out = [];
2962
2975
  for (const mb of messageBlocks) {
2963
- if (!isRecord7(mb)) {
2976
+ if (!isRecord8(mb)) {
2964
2977
  continue;
2965
2978
  }
2966
- const message = isRecord7(mb.message) ? mb.message : null;
2979
+ const message = isRecord8(mb.message) ? mb.message : null;
2967
2980
  if (!message) {
2968
2981
  continue;
2969
2982
  }
2970
- const messageText = getString2(message.text).trim();
2983
+ const messageText = getString3(message.text).trim();
2971
2984
  const blocksContent = extractMrkdwnFromBlocks(message.blocks).trim();
2972
2985
  const attachmentsContent = extractMrkdwnFromAttachments(message.attachments, nextState(state)).trim();
2973
2986
  const fileMentions = extractFileMentions(message.files).trim();
@@ -3013,11 +3026,11 @@ function extractFileMentions(files) {
3013
3026
  }
3014
3027
  const lines = [];
3015
3028
  for (const f of files) {
3016
- if (!isRecord7(f)) {
3029
+ if (!isRecord8(f)) {
3017
3030
  continue;
3018
3031
  }
3019
- const name = getString2(f.title) || getString2(f.name) || "file";
3020
- const url = getString2(f.permalink) || getString2(f.url_private_download) || getString2(f.url_private);
3032
+ const name = getString3(f.title) || getString3(f.name) || "file";
3033
+ const url = getString3(f.permalink) || getString3(f.url_private_download) || getString3(f.url_private);
3021
3034
  if (url) {
3022
3035
  lines.push(`<${url}|${name}>`);
3023
3036
  continue;
@@ -3027,66 +3040,14 @@ function extractFileMentions(files) {
3027
3040
  return uniqueTexts(lines).join(`
3028
3041
  `);
3029
3042
  }
3030
- function isRecord7(value) {
3043
+ function isRecord8(value) {
3031
3044
  return typeof value === "object" && value !== null;
3032
3045
  }
3033
- function getString2(value) {
3046
+ function getString3(value) {
3034
3047
  return typeof value === "string" ? value : "";
3035
3048
  }
3036
3049
 
3037
- // src/slack/message-api-parsing.ts
3038
- function toSlackFileSummary(value) {
3039
- if (!isRecord5(value)) {
3040
- return null;
3041
- }
3042
- const id = getString(value.id);
3043
- if (!id) {
3044
- return null;
3045
- }
3046
- return {
3047
- id,
3048
- name: getString(value.name),
3049
- title: getString(value.title),
3050
- mimetype: getString(value.mimetype),
3051
- filetype: getString(value.filetype),
3052
- mode: getString(value.mode),
3053
- permalink: getString(value.permalink),
3054
- url_private: getString(value.url_private),
3055
- url_private_download: getString(value.url_private_download),
3056
- size: getNumber(value.size)
3057
- };
3058
- }
3059
- async function enrichFiles(client, files) {
3060
- const out = [];
3061
- for (const f of files) {
3062
- if (f.mode === "snippet" || !f.url_private_download) {
3063
- try {
3064
- const info = await client.api("files.info", { file: f.id });
3065
- const file = isRecord5(info.file) ? info.file : null;
3066
- out.push({
3067
- ...f,
3068
- name: f.name ?? getString(file?.name),
3069
- title: f.title ?? getString(file?.title),
3070
- mimetype: f.mimetype ?? getString(file?.mimetype),
3071
- filetype: f.filetype ?? getString(file?.filetype),
3072
- mode: f.mode ?? getString(file?.mode),
3073
- permalink: f.permalink ?? getString(file?.permalink),
3074
- url_private: f.url_private ?? getString(file?.url_private),
3075
- url_private_download: f.url_private_download ?? getString(file?.url_private_download),
3076
- snippet: {
3077
- content: getString(file?.content),
3078
- language: getString(file?.filetype)
3079
- }
3080
- });
3081
- continue;
3082
- } catch {}
3083
- }
3084
- out.push(f);
3085
- }
3086
- return out;
3087
- }
3088
-
3089
- // src/slack/messages.ts
3050
+ // src/slack/message-compact.ts
3090
3051
  function toCompactMessage(msg, input) {
3091
3052
  const maxBodyChars = input?.maxBodyChars ?? 8000;
3092
3053
  const includeReactions = input?.includeReactions ?? false;
@@ -3174,6 +3135,8 @@ function extractForwardedThreads(attachments) {
3174
3135
  }
3175
3136
  return out.length ? out : undefined;
3176
3137
  }
3138
+
3139
+ // src/slack/messages.ts
3177
3140
  async function fetchMessage(client, input) {
3178
3141
  const history = await client.api("conversations.history", {
3179
3142
  channel: input.ref.channel_id,
@@ -3444,28 +3407,242 @@ function parseMsgTarget(input) {
3444
3407
  return { kind: "channel", channel: `#${trimmed}` };
3445
3408
  }
3446
3409
 
3447
- // src/cli/message-file-downloads.ts
3448
- import { readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
3449
- import { join as join10 } from "node:path";
3450
- function inferFileExtension(file) {
3451
- const mt = (file.mimetype || "").toLowerCase();
3452
- const ft = (file.filetype || "").toLowerCase();
3453
- if (mt === "image/png" || ft === "png") {
3454
- return "png";
3455
- }
3456
- if (mt === "image/jpeg" || mt === "image/jpg" || ft === "jpg" || ft === "jpeg") {
3457
- return "jpg";
3458
- }
3459
- if (mt === "image/webp" || ft === "webp") {
3460
- return "webp";
3410
+ // src/cli/message-url-warning.ts
3411
+ function warnOnTruncatedSlackUrl(ref) {
3412
+ if (ref.possiblyTruncated) {
3413
+ console.error(`Hint: URL may have been truncated by shell. Quote URLs containing "&":
3414
+ ` + ' agent-slack message get "https://...?thread_ts=...&cid=..."');
3461
3415
  }
3462
- if (mt === "image/gif" || ft === "gif") {
3463
- return "gif";
3416
+ }
3417
+
3418
+ // src/slack/rich-text.ts
3419
+ var BULLET_RE = /^(\s*)[•◦▪▫▸‣●○◆◇\-*]\s+(.*)$/;
3420
+ var ORDERED_RE = /^(\s*)\d+[.)]\s+(.*)$/;
3421
+ var CODE_BLOCK_START = /^```/;
3422
+ var BLOCKQUOTE_RE = /^> (.*)$/;
3423
+ function parseInlineElements(text) {
3424
+ const elements = [];
3425
+ const re = /`([^`]+)`|\*([^*]+)\*|_([^_]+)_|~([^~]+)~|<([^>|]+)\|([^>]+)>|<([^>|]+)>/g;
3426
+ let lastIndex = 0;
3427
+ let match;
3428
+ while ((match = re.exec(text)) !== null) {
3429
+ if (match.index > lastIndex) {
3430
+ elements.push({ type: "text", text: text.slice(lastIndex, match.index) });
3431
+ }
3432
+ const [, code, bold, italic, strike, linkUrl, linkText, bareUrl] = match;
3433
+ if (code != null) {
3434
+ elements.push({ type: "text", text: code, style: { code: true } });
3435
+ } else if (bold != null) {
3436
+ elements.push({ type: "text", text: bold, style: { bold: true } });
3437
+ } else if (italic != null) {
3438
+ elements.push({ type: "text", text: italic, style: { italic: true } });
3439
+ } else if (strike != null) {
3440
+ elements.push({ type: "text", text: strike, style: { strike: true } });
3441
+ } else if (linkUrl != null && linkText != null) {
3442
+ elements.push({ type: "link", url: linkUrl, text: linkText });
3443
+ } else if (bareUrl != null) {
3444
+ elements.push({ type: "link", url: bareUrl });
3445
+ }
3446
+ lastIndex = match.index + match[0].length;
3447
+ }
3448
+ if (lastIndex < text.length) {
3449
+ elements.push({ type: "text", text: text.slice(lastIndex) });
3450
+ }
3451
+ return elements.length > 0 ? elements : [{ type: "text", text }];
3452
+ }
3453
+ function textToRichTextBlocks(text) {
3454
+ const lines = text.split(`
3455
+ `);
3456
+ const elements = [];
3457
+ let hasLists = false;
3458
+ let idx = 0;
3459
+ while (idx < lines.length) {
3460
+ const line = lines[idx];
3461
+ if (CODE_BLOCK_START.test(line)) {
3462
+ idx++;
3463
+ const codeLines = [];
3464
+ while (idx < lines.length && !CODE_BLOCK_START.test(lines[idx])) {
3465
+ codeLines.push(lines[idx]);
3466
+ idx++;
3467
+ }
3468
+ if (idx < lines.length) {
3469
+ idx++;
3470
+ }
3471
+ elements.push({
3472
+ type: "rich_text_preformatted",
3473
+ elements: [{ type: "text", text: codeLines.join(`
3474
+ `) }]
3475
+ });
3476
+ continue;
3477
+ }
3478
+ const quoteMatch = line.match(BLOCKQUOTE_RE);
3479
+ if (quoteMatch) {
3480
+ const quoteLines = [];
3481
+ while (idx < lines.length) {
3482
+ const qm = lines[idx].match(BLOCKQUOTE_RE);
3483
+ if (!qm) {
3484
+ break;
3485
+ }
3486
+ quoteLines.push(qm[1]);
3487
+ idx++;
3488
+ }
3489
+ elements.push({
3490
+ type: "rich_text_quote",
3491
+ elements: parseInlineElements(quoteLines.join(`
3492
+ `))
3493
+ });
3494
+ continue;
3495
+ }
3496
+ if (BULLET_RE.test(line)) {
3497
+ hasLists = true;
3498
+ idx = collectList({ lines, startIdx: idx, style: "bullet", pattern: BULLET_RE, elements });
3499
+ continue;
3500
+ }
3501
+ if (ORDERED_RE.test(line)) {
3502
+ hasLists = true;
3503
+ idx = collectList({ lines, startIdx: idx, style: "ordered", pattern: ORDERED_RE, elements });
3504
+ continue;
3505
+ }
3506
+ const textLines = [];
3507
+ while (idx < lines.length) {
3508
+ const l = lines[idx];
3509
+ if (BULLET_RE.test(l) || ORDERED_RE.test(l) || CODE_BLOCK_START.test(l) || BLOCKQUOTE_RE.test(l)) {
3510
+ break;
3511
+ }
3512
+ textLines.push(l);
3513
+ idx++;
3514
+ }
3515
+ const content = textLines.join(`
3516
+ `);
3517
+ if (content.trim()) {
3518
+ elements.push({
3519
+ type: "rich_text_section",
3520
+ elements: parseInlineElements(content.endsWith(`
3521
+ `) ? content : `${content}
3522
+ `)
3523
+ });
3524
+ }
3464
3525
  }
3465
- if (mt === "text/plain" || ft === "text") {
3466
- return "txt";
3526
+ if (!hasLists) {
3527
+ return null;
3467
3528
  }
3468
- if (mt === "text/markdown" || ft === "markdown" || ft === "md") {
3529
+ return [{ type: "rich_text", elements }];
3530
+ }
3531
+ function collectList(input) {
3532
+ const { lines, startIdx, style, pattern, elements } = input;
3533
+ let idx = startIdx;
3534
+ const firstMatch = lines[startIdx].match(pattern);
3535
+ const baseIndent = firstMatch[1].length;
3536
+ let currentIndent = -1;
3537
+ let currentItems = [];
3538
+ while (idx < lines.length) {
3539
+ const match = lines[idx].match(pattern);
3540
+ if (!match) {
3541
+ break;
3542
+ }
3543
+ const indent = match[1].length >= baseIndent + 2 ? 1 : 0;
3544
+ const content = match[2];
3545
+ if (currentIndent !== -1 && indent !== currentIndent) {
3546
+ elements.push({
3547
+ type: "rich_text_list",
3548
+ style,
3549
+ ...currentIndent > 0 ? { indent: currentIndent } : {},
3550
+ elements: currentItems
3551
+ });
3552
+ currentItems = [];
3553
+ }
3554
+ currentIndent = indent;
3555
+ currentItems.push({
3556
+ type: "rich_text_section",
3557
+ elements: parseInlineElements(content)
3558
+ });
3559
+ idx++;
3560
+ }
3561
+ if (currentItems.length > 0) {
3562
+ elements.push({
3563
+ type: "rich_text_list",
3564
+ style,
3565
+ ...currentIndent > 0 ? { indent: currentIndent } : {},
3566
+ elements: currentItems
3567
+ });
3568
+ }
3569
+ return idx;
3570
+ }
3571
+
3572
+ // src/slack/upload.ts
3573
+ import { readFile as readFile5, stat, realpath } from "node:fs/promises";
3574
+ import { basename as basename2 } from "node:path";
3575
+ var MAX_FILE_SIZE = 100 * 1024 * 1024;
3576
+ async function uploadLocalFileToSlack(input) {
3577
+ const resolvedPath = await realpath(input.filePath);
3578
+ const fileStats = await stat(resolvedPath);
3579
+ if (!fileStats.isFile()) {
3580
+ throw new Error(`Attachment path is not a file: ${input.filePath}`);
3581
+ }
3582
+ if (fileStats.size > MAX_FILE_SIZE) {
3583
+ throw new Error(`File too large (${Math.round(fileStats.size / 1024 / 1024)}MB). Slack allows up to 100MB.`);
3584
+ }
3585
+ const bytes = await readFile5(resolvedPath);
3586
+ const filename = basename2(resolvedPath);
3587
+ const uploadInitResp = await input.client.api("files.getUploadURLExternal", {
3588
+ filename,
3589
+ length: bytes.length
3590
+ });
3591
+ if (isRecord5(uploadInitResp) && uploadInitResp.ok === false) {
3592
+ const errMsg = typeof uploadInitResp.error === "string" ? uploadInitResp.error : "unknown";
3593
+ throw new Error(`Slack files.getUploadURLExternal failed: ${errMsg}`);
3594
+ }
3595
+ const uploadUrl = getString(uploadInitResp.upload_url);
3596
+ const fileId = getString(uploadInitResp.file_id);
3597
+ if (!uploadUrl || !fileId) {
3598
+ throw new Error("Slack did not return an upload URL for file attachment");
3599
+ }
3600
+ const uploadResp = await fetch(uploadUrl, {
3601
+ method: "POST",
3602
+ headers: {
3603
+ "Content-Type": "application/octet-stream",
3604
+ "Content-Length": String(bytes.length)
3605
+ },
3606
+ body: bytes
3607
+ });
3608
+ if (!uploadResp.ok) {
3609
+ const body = await uploadResp.text().catch(() => "");
3610
+ throw new Error(`Failed to upload attachment bytes (HTTP ${uploadResp.status})${body ? `: ${body}` : ""}`);
3611
+ }
3612
+ const completeResp = await input.client.api("files.completeUploadExternal", {
3613
+ files: [{ id: fileId, title: filename }],
3614
+ channel_id: input.channelId,
3615
+ thread_ts: input.threadTs,
3616
+ initial_comment: input.initialComment?.trim() || undefined
3617
+ });
3618
+ if (!isRecord5(completeResp) || completeResp.ok !== true) {
3619
+ const errMsg = isRecord5(completeResp) && typeof completeResp.error === "string" ? completeResp.error : "unknown";
3620
+ throw new Error(`Slack files.completeUploadExternal failed: ${errMsg}`);
3621
+ }
3622
+ }
3623
+
3624
+ // src/cli/message-file-downloads.ts
3625
+ import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
3626
+ import { join as join11 } from "node:path";
3627
+ function inferFileExtension(file) {
3628
+ const mt = (file.mimetype || "").toLowerCase();
3629
+ const ft = (file.filetype || "").toLowerCase();
3630
+ if (mt === "image/png" || ft === "png") {
3631
+ return "png";
3632
+ }
3633
+ if (mt === "image/jpeg" || mt === "image/jpg" || ft === "jpg" || ft === "jpeg") {
3634
+ return "jpg";
3635
+ }
3636
+ if (mt === "image/webp" || ft === "webp") {
3637
+ return "webp";
3638
+ }
3639
+ if (mt === "image/gif" || ft === "gif") {
3640
+ return "gif";
3641
+ }
3642
+ if (mt === "text/plain" || ft === "text") {
3643
+ return "txt";
3644
+ }
3645
+ if (mt === "text/markdown" || ft === "markdown" || ft === "md") {
3469
3646
  return "md";
3470
3647
  }
3471
3648
  if (mt === "application/json" || ft === "json") {
@@ -3487,13 +3664,13 @@ async function downloadCanvasAsMarkdown(input) {
3487
3664
  preferredName: `${input.fileId}.html`,
3488
3665
  options: { allowHtml: true }
3489
3666
  });
3490
- const html = await readFile5(htmlPath, "utf8");
3667
+ const html = await readFile6(htmlPath, "utf8");
3491
3668
  if (looksLikeAuthPage(html)) {
3492
3669
  throw new Error("Downloaded auth/login page instead of canvas content (token may be expired)");
3493
3670
  }
3494
3671
  const markdown = htmlToMarkdown(html).trim();
3495
3672
  const safeName = `${input.fileId.replace(/[\\/<>"|?*]/g, "_")}.md`;
3496
- const markdownPath = join10(input.destDir, safeName);
3673
+ const markdownPath = join11(input.destDir, safeName);
3497
3674
  await writeFile3(markdownPath, markdown, "utf8");
3498
3675
  return markdownPath;
3499
3676
  }
@@ -3535,14 +3712,6 @@ async function downloadMessageFiles(input) {
3535
3712
  return downloadedPaths;
3536
3713
  }
3537
3714
 
3538
- // src/cli/message-url-warning.ts
3539
- function warnOnTruncatedSlackUrl(ref) {
3540
- if (ref.possiblyTruncated) {
3541
- console.error(`Hint: URL may have been truncated by shell. Quote URLs containing "&":
3542
- ` + ' agent-slack message get "https://...?thread_ts=...&cid=..."');
3543
- }
3544
- }
3545
-
3546
3715
  // src/cli/message-thread-info.ts
3547
3716
  function getNumber2(value) {
3548
3717
  return typeof value === "number" ? value : undefined;
@@ -3573,48 +3742,7 @@ function toThreadListMessage(m) {
3573
3742
  return rest;
3574
3743
  }
3575
3744
 
3576
- // src/cli/message-actions.ts
3577
- function parseLimit(raw) {
3578
- if (raw === undefined) {
3579
- return;
3580
- }
3581
- const n = Number.parseInt(raw, 10);
3582
- if (!Number.isFinite(n) || n < 1) {
3583
- throw new Error(`Invalid --limit value "${raw}": must be a positive integer`);
3584
- }
3585
- return n;
3586
- }
3587
- function requireMessageTs(raw) {
3588
- const ts = raw?.trim();
3589
- if (!ts) {
3590
- throw new Error('When targeting a channel, you must pass --ts "<seconds>.<micros>"');
3591
- }
3592
- return ts;
3593
- }
3594
- function parseReactionFilters(raw) {
3595
- if (!Array.isArray(raw) || raw.length === 0) {
3596
- return [];
3597
- }
3598
- const out = [];
3599
- for (const value of raw) {
3600
- const normalized = normalizeSlackReactionName(String(value));
3601
- if (!out.includes(normalized)) {
3602
- out.push(normalized);
3603
- }
3604
- }
3605
- return out;
3606
- }
3607
- function requireOldestWhenReactionFiltersUsed(input) {
3608
- const hasReactionFilters = input.withReactions.length > 0 || input.withoutReactions.length > 0;
3609
- const oldest = input.oldest?.trim();
3610
- if (!hasReactionFilters) {
3611
- return oldest;
3612
- }
3613
- if (!oldest) {
3614
- throw new Error('Reaction filters require --oldest "<seconds>.<micros>" to bound scan size. Example: --oldest "1770165109.628379"');
3615
- }
3616
- return oldest;
3617
- }
3745
+ // src/cli/message-read-actions.ts
3618
3746
  async function handleMessageGet(input) {
3619
3747
  const target = parseMsgTarget(input.targetInput);
3620
3748
  if (target.kind === "user") {
@@ -3756,8 +3884,53 @@ async function handleMessageList(input) {
3756
3884
  }
3757
3885
  });
3758
3886
  }
3887
+
3888
+ // src/cli/message-actions.ts
3889
+ function parseLimit(raw) {
3890
+ if (raw === undefined) {
3891
+ return;
3892
+ }
3893
+ const n = Number.parseInt(raw, 10);
3894
+ if (!Number.isFinite(n) || n < 1) {
3895
+ throw new Error(`Invalid --limit value "${raw}": must be a positive integer`);
3896
+ }
3897
+ return n;
3898
+ }
3899
+ function requireMessageTs(raw) {
3900
+ const ts = raw?.trim();
3901
+ if (!ts) {
3902
+ throw new Error('When targeting a channel, you must pass --ts "<seconds>.<micros>"');
3903
+ }
3904
+ return ts;
3905
+ }
3906
+ function parseReactionFilters(raw) {
3907
+ if (!Array.isArray(raw) || raw.length === 0) {
3908
+ return [];
3909
+ }
3910
+ const out = [];
3911
+ for (const value of raw) {
3912
+ const normalized = normalizeSlackReactionName(String(value));
3913
+ if (!out.includes(normalized)) {
3914
+ out.push(normalized);
3915
+ }
3916
+ }
3917
+ return out;
3918
+ }
3919
+ function requireOldestWhenReactionFiltersUsed(input) {
3920
+ const hasReactionFilters = input.withReactions.length > 0 || input.withoutReactions.length > 0;
3921
+ const oldest = input.oldest?.trim();
3922
+ if (!hasReactionFilters) {
3923
+ return oldest;
3924
+ }
3925
+ if (!oldest) {
3926
+ throw new Error('Reaction filters require --oldest "<seconds>.<micros>" to bound scan size. Example: --oldest "1770165109.628379"');
3927
+ }
3928
+ return oldest;
3929
+ }
3759
3930
  async function sendMessage(input) {
3760
3931
  const target = parseMsgTarget(String(input.targetInput));
3932
+ const blocks = input.text ? textToRichTextBlocks(input.text) : null;
3933
+ const attachPaths = normalizeAttachPaths(input.options.attach);
3761
3934
  if (target.kind === "url") {
3762
3935
  const { ref } = target;
3763
3936
  warnOnTruncatedSlackUrl(ref);
@@ -3767,10 +3940,13 @@ async function sendMessage(input) {
3767
3940
  const { client } = await input.ctx.getClientForWorkspace(ref.workspace_url);
3768
3941
  const msg = await fetchMessage(client, { ref });
3769
3942
  const threadTs = msg.thread_ts ?? msg.ts;
3770
- await client.api("chat.postMessage", {
3771
- channel: ref.channel_id,
3943
+ await sendMessageToChannel({
3944
+ client,
3945
+ channelId: ref.channel_id,
3772
3946
  text: input.text,
3773
- thread_ts: threadTs
3947
+ blocks,
3948
+ threadTs,
3949
+ attachPaths
3774
3950
  });
3775
3951
  }
3776
3952
  });
@@ -3783,9 +3959,12 @@ async function sendMessage(input) {
3783
3959
  work: async () => {
3784
3960
  const { client } = await input.ctx.getClientForWorkspace(workspaceUrl2);
3785
3961
  const dmChannelId = await openDmChannel(client, target.userId);
3786
- await client.api("chat.postMessage", {
3787
- channel: dmChannelId,
3788
- text: input.text
3962
+ await sendMessageToChannel({
3963
+ client,
3964
+ channelId: dmChannelId,
3965
+ text: input.text,
3966
+ blocks,
3967
+ attachPaths
3789
3968
  });
3790
3969
  }
3791
3970
  });
@@ -3801,15 +3980,56 @@ async function sendMessage(input) {
3801
3980
  work: async () => {
3802
3981
  const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
3803
3982
  const channelId = await resolveChannelId(client, String(target.channel));
3804
- await client.api("chat.postMessage", {
3805
- channel: channelId,
3983
+ await sendMessageToChannel({
3984
+ client,
3985
+ channelId,
3806
3986
  text: input.text,
3807
- thread_ts: input.options.threadTs ? String(input.options.threadTs) : undefined
3987
+ blocks,
3988
+ threadTs: input.options.threadTs ? String(input.options.threadTs) : undefined,
3989
+ attachPaths
3808
3990
  });
3809
3991
  }
3810
3992
  });
3811
3993
  return { ok: true };
3812
3994
  }
3995
+ function normalizeAttachPaths(raw) {
3996
+ if (!Array.isArray(raw) || raw.length === 0) {
3997
+ return [];
3998
+ }
3999
+ const out = [];
4000
+ for (const p of raw.map((v) => String(v).trim()).filter(Boolean)) {
4001
+ if (!out.includes(p)) {
4002
+ out.push(p);
4003
+ }
4004
+ }
4005
+ return out;
4006
+ }
4007
+ async function sendMessageToChannel(input) {
4008
+ if (input.attachPaths.length === 0) {
4009
+ await input.client.api("chat.postMessage", {
4010
+ channel: input.channelId,
4011
+ text: input.text,
4012
+ thread_ts: input.threadTs,
4013
+ ...input.blocks ? { blocks: input.blocks } : {}
4014
+ });
4015
+ return;
4016
+ }
4017
+ if (input.blocks) {
4018
+ process.stderr.write(`Warning: rich text formatting is not supported with file attachments; sending as plain text.
4019
+ `);
4020
+ }
4021
+ let initialComment = input.text;
4022
+ for (const filePath of input.attachPaths) {
4023
+ await uploadLocalFileToSlack({
4024
+ client: input.client,
4025
+ channelId: input.channelId,
4026
+ filePath,
4027
+ threadTs: input.threadTs,
4028
+ initialComment
4029
+ });
4030
+ initialComment = "";
4031
+ }
4032
+ }
3813
4033
  async function editMessage(input) {
3814
4034
  const target = parseMsgTarget(String(input.targetInput));
3815
4035
  if (target.kind === "user") {
@@ -3904,1403 +4124,164 @@ async function reactOnTarget(input) {
3904
4124
  const ts = requireMessageTs(input.options?.ts);
3905
4125
  await input.ctx.assertWorkspaceSpecifiedForChannelNames({
3906
4126
  workspaceUrl,
3907
- channels: [target.channel]
3908
- });
3909
- const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
3910
- const channelId = await resolveChannelId(client, target.channel);
3911
- const name = normalizeSlackReactionName(input.emoji);
3912
- await client.api(`reactions.${input.action}`, {
3913
- channel: channelId,
3914
- timestamp: ts,
3915
- name
3916
- });
3917
- }
3918
- });
3919
- return { ok: true };
3920
- }
3921
-
3922
- // src/cli/draft-server.ts
3923
- import { createServer } from "node:http";
3924
- import { exec } from "node:child_process";
3925
- function openDraftEditor(config) {
3926
- return new Promise((resolve3, reject) => {
3927
- let settled = false;
3928
- const server = createServer(async (req, res) => {
3929
- if (req.method === "GET" && (req.url === "/" || req.url === "/index.html")) {
3930
- const html = buildEditorHtml(config);
3931
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
3932
- res.end(html);
3933
- return;
3934
- }
3935
- if (req.method === "POST" && req.url === "/send") {
3936
- try {
3937
- const body = await readBody(req);
3938
- const data = JSON.parse(body);
3939
- if (typeof data.text !== "string" || !data.text.trim()) {
3940
- res.writeHead(400, { "Content-Type": "application/json" });
3941
- res.end(JSON.stringify({ ok: false, error: "text is required" }));
3942
- return;
3943
- }
3944
- const sendResult = await config.onSend(data.text);
3945
- res.writeHead(200, { "Content-Type": "application/json" });
3946
- res.end(JSON.stringify({ ok: true, ts: sendResult.ts }));
3947
- settled = true;
3948
- resolve3({ sent: true, text: data.text });
3949
- setTimeout(() => server.close(), 300);
3950
- } catch (err) {
3951
- res.writeHead(500, { "Content-Type": "application/json" });
3952
- res.end(JSON.stringify({ ok: false, error: String(err) }));
3953
- }
3954
- return;
3955
- }
3956
- if (req.method === "POST" && req.url === "/cancel") {
3957
- res.writeHead(200, { "Content-Type": "application/json" });
3958
- res.end(JSON.stringify({ ok: true }));
3959
- settled = true;
3960
- resolve3({ cancelled: true });
3961
- setTimeout(() => server.close(), 300);
3962
- return;
3963
- }
3964
- res.writeHead(404);
3965
- res.end("Not found");
3966
- });
3967
- server.on("error", (err) => {
3968
- if (!settled) {
3969
- reject(err);
3970
- }
3971
- });
3972
- server.on("close", () => {
3973
- clearTimeout(idleTimeout);
3974
- if (!settled) {
3975
- settled = true;
3976
- resolve3({ cancelled: true });
3977
- }
3978
- });
3979
- const idleTimeout = setTimeout(() => {
3980
- if (!settled) {
3981
- server.close();
3982
- }
3983
- }, 30 * 60 * 1000);
3984
- server.listen(0, "127.0.0.1", () => {
3985
- const addr = server.address();
3986
- const port = typeof addr === "object" && addr ? addr.port : 0;
3987
- const url = `http://127.0.0.1:${port}`;
3988
- process.stderr.write(`Draft editor: ${url}
3989
- `);
3990
- openBrowser(url);
3991
- });
3992
- });
3993
- }
3994
- function readBody(req) {
3995
- return new Promise((resolve3, reject) => {
3996
- const chunks = [];
3997
- req.on("data", (chunk) => chunks.push(chunk));
3998
- req.on("end", () => resolve3(Buffer.concat(chunks).toString()));
3999
- req.on("error", reject);
4000
- });
4001
- }
4002
- function openBrowser(url) {
4003
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
4004
- exec(`${cmd} "${url}"`, () => {});
4005
- }
4006
- function buildSlackThreadUrl(config) {
4007
- if (!config.workspaceUrl || !config.channelId || !config.threadTs) {
4008
- return null;
4009
- }
4010
- const tsNoDot = config.threadTs.replace(".", "");
4011
- return `${config.workspaceUrl.replace(/\/$/, "")}/archives/${config.channelId}/p${tsNoDot}`;
4012
- }
4013
- function extractWorkspaceName(url) {
4014
- if (!url) {
4015
- return null;
4016
- }
4017
- try {
4018
- const host = new URL(url).hostname;
4019
- const parts = host.split(".");
4020
- if (parts.length >= 3 && parts.at(-2) === "slack") {
4021
- return parts.slice(0, -2).join(".");
4022
- }
4023
- return host;
4024
- } catch {
4025
- return null;
4026
- }
4027
- }
4028
- function buildEditorHtml(config) {
4029
- const threadUrl = buildSlackThreadUrl(config);
4030
- const workspaceName = extractWorkspaceName(config.workspaceUrl);
4031
- const injectedConfig = JSON.stringify({
4032
- channelName: config.channelName,
4033
- channelId: config.channelId || null,
4034
- workspaceUrl: config.workspaceUrl || null,
4035
- workspaceName,
4036
- threadTs: config.threadTs || null,
4037
- threadUrl,
4038
- initialText: config.initialText || ""
4039
- });
4040
- return EDITOR_HTML.replace("__DRAFT_CONFIG__", injectedConfig.replace(/</g, "\\u003c"));
4041
- }
4042
- var EDITOR_HTML = `<!DOCTYPE html>
4043
- <html lang="en">
4044
- <head>
4045
- <meta charset="UTF-8">
4046
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
4047
- <title>Draft Message — Agent Slack</title>
4048
- <link rel="preconnect" href="https://fonts.googleapis.com">
4049
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
4050
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap" rel="stylesheet">
4051
- <style>
4052
- :root {
4053
- --bg: #1a1d21;
4054
- --surface: #222529;
4055
- --surface-raised: #2c2d31;
4056
- --border: #393b3f;
4057
- --border-focus: #1264a3;
4058
- --text: #d1d2d3;
4059
- --text-secondary: #ababad;
4060
- --text-muted: #696a6d;
4061
- --green: #007a5a;
4062
- --green-hover: #148567;
4063
- --red: #e01e5a;
4064
- --blue: #1264a3;
4065
- --blue-link: #1d9bd1;
4066
- --code-bg: #1a1d21;
4067
- --blockquote-border: #616061;
4068
- --toolbar-active: rgba(29,155,209,0.15);
4069
- }
4070
-
4071
- * { margin: 0; padding: 0; box-sizing: border-box; }
4072
-
4073
- body {
4074
- font-family: Slack-Lato, Lato, appleLogo, sans-serif;
4075
- background: var(--bg);
4076
- color: var(--text);
4077
- display: flex;
4078
- align-items: flex-start;
4079
- justify-content: center;
4080
- min-height: 100vh;
4081
- padding: 40px 24px;
4082
- }
4083
-
4084
- .page {
4085
- width: 100%;
4086
- max-width: 720px;
4087
- display: flex;
4088
- flex-direction: column;
4089
- align-items: center;
4090
- animation: fadeIn 0.18s ease-out;
4091
- }
4092
-
4093
- @keyframes fadeIn {
4094
- from { opacity: 0; transform: translateY(6px); }
4095
- to { opacity: 1; transform: translateY(0); }
4096
- }
4097
-
4098
- /* ── Branding ── */
4099
- .brand {
4100
- position: relative;
4101
- text-align: center;
4102
- margin-bottom: 20px;
4103
- user-select: none;
4104
- }
4105
- .brand-name {
4106
- font-size: 17px;
4107
- font-weight: 900;
4108
- letter-spacing: -0.3px;
4109
- color: var(--text);
4110
- }
4111
- .brand-byline {
4112
- display: flex;
4113
- align-items: center;
4114
- gap: 6px;
4115
- margin-top: 2px;
4116
- justify-content: center;
4117
- font-family: 'Inter', sans-serif;
4118
- font-size: 10px;
4119
- font-weight: 500;
4120
- color: rgba(255,255,255,0.5);
4121
- }
4122
- .brand-byline a {
4123
- color: rgba(255,255,255,0.6);
4124
- text-decoration: none;
4125
- }
4126
- .brand-byline a:hover {
4127
- color: rgba(255,255,255,0.8);
4128
- }
4129
- .brand-sub {
4130
- font-size: 11px;
4131
- font-weight: 600;
4132
- letter-spacing: 1.5px;
4133
- text-transform: uppercase;
4134
- color: var(--text-muted);
4135
- margin-top: 8px;
4136
- }
4137
- .brand-byline img {
4138
- width: 12px;
4139
- height: 12px;
4140
- opacity: 0.6;
4141
- vertical-align: -1px;
4142
- }
4143
-
4144
- /* ── Context bar ── */
4145
- .context-bar {
4146
- width: 100%;
4147
- display: flex;
4148
- align-items: center;
4149
- gap: 6px;
4150
- padding: 0 4px;
4151
- margin-bottom: 10px;
4152
- font-size: 13px;
4153
- color: var(--text-secondary);
4154
- }
4155
-
4156
- .context-bar .hash-icon {
4157
- width: 15px;
4158
- height: 15px;
4159
- color: var(--text-muted);
4160
- flex-shrink: 0;
4161
- }
4162
-
4163
- .context-bar .channel {
4164
- font-weight: 700;
4165
- color: var(--text);
4166
- }
4167
-
4168
- .context-bar .workspace-label {
4169
- color: var(--text-muted);
4170
- font-size: 13px;
4171
- margin-left: 8px;
4172
- }
4173
-
4174
- .context-bar .thread-link {
4175
- margin-left: auto;
4176
- font-size: 12px;
4177
- color: var(--blue-link);
4178
- text-decoration: none;
4179
- display: flex;
4180
- align-items: center;
4181
- gap: 4px;
4182
- }
4183
- .context-bar .thread-link:hover { text-decoration: underline; }
4184
- .context-bar .thread-link svg {
4185
- width: 12px;
4186
- height: 12px;
4187
- fill: none;
4188
- stroke: currentColor;
4189
- stroke-width: 2;
4190
- }
4191
-
4192
- /* ── Composer card ── */
4193
- .composer {
4194
- width: 100%;
4195
- background: var(--surface);
4196
- border: 1px solid #818385;
4197
- border-radius: 8px;
4198
- overflow: hidden;
4199
- transition: border-color 0.15s;
4200
- }
4201
-
4202
- .composer:focus-within {
4203
- border-color: var(--border-focus);
4204
- }
4205
-
4206
- /* ── Toolbar ── */
4207
- .toolbar {
4208
- display: flex;
4209
- align-items: center;
4210
- gap: 1px;
4211
- padding: 4px 8px;
4212
- border-bottom: 1px solid var(--border);
4213
- background: var(--surface);
4214
- }
4215
-
4216
- .toolbar-btn {
4217
- display: inline-flex;
4218
- align-items: center;
4219
- justify-content: center;
4220
- width: 32px;
4221
- height: 32px;
4222
- border: none;
4223
- border-radius: 4px;
4224
- background: transparent;
4225
- color: var(--text-secondary);
4226
- cursor: pointer;
4227
- font-size: 13px;
4228
- font-family: inherit;
4229
- transition: all 0.08s;
4230
- position: relative;
4231
- flex-shrink: 0;
4232
- }
4233
-
4234
- .toolbar-btn:hover {
4235
- background: var(--surface-raised);
4236
- color: var(--text);
4237
- }
4238
-
4239
- .toolbar-btn.active {
4240
- background: var(--toolbar-active);
4241
- color: var(--blue-link);
4242
- }
4243
-
4244
- .toolbar-btn.disabled {
4245
- opacity: 0.3;
4246
- pointer-events: none;
4247
- }
4248
-
4249
- .toolbar-btn svg { width: 16px; height: 16px; fill: currentColor; }
4250
- .toolbar-btn .b { font-weight: 800; font-size: 15px; line-height: 1; }
4251
- .toolbar-btn .i { font-style: italic; font-weight: 600; font-size: 15px; font-family: Georgia, serif; line-height: 1; }
4252
- .toolbar-btn .s { text-decoration: line-through; font-weight: 600; font-size: 13px; line-height: 1; }
4253
-
4254
- .toolbar-sep {
4255
- width: 1px;
4256
- height: 20px;
4257
- background: var(--border);
4258
- margin: 0 4px;
4259
- flex-shrink: 0;
4260
- }
4261
-
4262
- /* Tooltip */
4263
- .toolbar-btn[data-tip]::after {
4264
- content: attr(data-tip);
4265
- position: absolute;
4266
- bottom: calc(100% + 6px);
4267
- left: 50%;
4268
- transform: translateX(-50%);
4269
- background: #1d1d1d;
4270
- color: #e0e0e0;
4271
- font-size: 11px;
4272
- font-weight: 400;
4273
- font-style: normal;
4274
- text-decoration: none;
4275
- padding: 4px 8px;
4276
- border-radius: 6px;
4277
- white-space: nowrap;
4278
- pointer-events: none;
4279
- opacity: 0;
4280
- transition: opacity 0.12s;
4281
- z-index: 20;
4282
- }
4283
- .toolbar-btn:hover[data-tip]::after { opacity: 1; }
4284
-
4285
- /* ── Editor ── */
4286
- .editor {
4287
- min-height: 200px;
4288
- max-height: 55vh;
4289
- overflow-y: auto;
4290
- padding: 12px 16px;
4291
- font-size: 15px;
4292
- line-height: 1.46668;
4293
- color: var(--text);
4294
- outline: none;
4295
- word-wrap: break-word;
4296
- overflow-wrap: break-word;
4297
- }
4298
-
4299
- .editor:empty::before {
4300
- content: attr(data-placeholder);
4301
- color: var(--text-muted);
4302
- pointer-events: none;
4303
- }
4304
-
4305
- .editor b, .editor strong { font-weight: 700; }
4306
- .editor i, .editor em { font-style: italic; }
4307
- .editor s, .editor strike, .editor del { text-decoration: line-through; }
4308
-
4309
- .editor code {
4310
- background: var(--code-bg);
4311
- border: 1px solid var(--border);
4312
- border-radius: 3px;
4313
- padding: 2px 4px;
4314
- font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;
4315
- font-size: 12px;
4316
- color: #e06c75;
4317
- }
4318
-
4319
- .editor pre {
4320
- background: var(--code-bg);
4321
- border: 1px solid var(--border);
4322
- border-radius: 4px;
4323
- padding: 8px 12px;
4324
- margin: 4px 0;
4325
- font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;
4326
- font-size: 12px;
4327
- line-height: 1.5;
4328
- overflow-x: auto;
4329
- white-space: pre-wrap;
4330
- color: var(--text);
4331
- }
4332
-
4333
- .editor pre code {
4334
- background: none;
4335
- border: none;
4336
- padding: 0;
4337
- font-size: inherit;
4338
- color: inherit;
4339
- }
4340
-
4341
- /* Adjacent <pre> elements look like one code block (browser splits on Enter) */
4342
- .editor pre + pre,
4343
- .editor pre + br + pre {
4344
- border-top: none;
4345
- margin-top: -4px; /* collapse gap */
4346
- padding-top: 0;
4347
- border-top-left-radius: 0;
4348
- border-top-right-radius: 0;
4349
- }
4350
- .editor pre:has(+ pre),
4351
- .editor pre:has(+ br + pre) {
4352
- border-bottom: none;
4353
- margin-bottom: 0;
4354
- padding-bottom: 0;
4355
- border-bottom-left-radius: 0;
4356
- border-bottom-right-radius: 0;
4357
- }
4358
-
4359
- .editor blockquote {
4360
- border-left: 4px solid var(--blockquote-border);
4361
- padding: 4px 0 4px 16px;
4362
- margin: 4px 0;
4363
- color: var(--text-secondary);
4364
- }
4365
-
4366
- .editor ul, .editor ol { padding-left: 26px; margin: 4px 0; }
4367
- .editor li { margin: 2px 0; }
4368
- .editor a { color: var(--blue-link); text-decoration: none; }
4369
- .editor a:hover { text-decoration: underline; }
4370
-
4371
- /* ── Source textarea ── */
4372
- .source-editor {
4373
- display: none;
4374
- min-height: 200px;
4375
- max-height: 55vh;
4376
- width: 100%;
4377
- padding: 12px 16px;
4378
- font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;
4379
- font-size: 13px;
4380
- line-height: 1.5;
4381
- background: var(--surface);
4382
- color: var(--text);
4383
- border: none;
4384
- outline: none;
4385
- resize: vertical;
4386
- }
4387
-
4388
- /* ── Bottom bar ── */
4389
- .bottom-bar {
4390
- display: flex;
4391
- align-items: center;
4392
- justify-content: space-between;
4393
- padding: 6px 10px;
4394
- border-top: 1px solid var(--border);
4395
- background: var(--surface);
4396
- }
4397
-
4398
- .bottom-left {
4399
- display: flex;
4400
- align-items: center;
4401
- gap: 10px;
4402
- }
4403
-
4404
- .hint {
4405
- font-size: 12px;
4406
- color: var(--text-muted);
4407
- }
4408
- .hint kbd {
4409
- background: var(--surface-raised);
4410
- border: 1px solid var(--border);
4411
- border-radius: 3px;
4412
- padding: 1px 4px;
4413
- font-family: inherit;
4414
- font-size: 11px;
4415
- }
4416
-
4417
- .btn-send {
4418
- background: var(--green);
4419
- color: #fff;
4420
- display: inline-flex;
4421
- align-items: center;
4422
- justify-content: center;
4423
- width: 32px;
4424
- height: 32px;
4425
- border: none;
4426
- border-radius: 4px;
4427
- cursor: pointer;
4428
- transition: all 0.1s;
4429
- flex-shrink: 0;
4430
- }
4431
- .btn-send:hover { background: var(--green-hover); }
4432
- .btn-send:disabled { opacity: 0.3; cursor: not-allowed; }
4433
- .btn-send svg { width: 16px; height: 16px; fill: currentColor; }
4434
-
4435
- .cancel-link {
4436
- background: none;
4437
- border: none;
4438
- color: var(--text-muted);
4439
- font-size: 12px;
4440
- font-family: inherit;
4441
- cursor: pointer;
4442
- padding: 4px 6px;
4443
- }
4444
- .cancel-link:hover { color: var(--text-secondary); text-decoration: underline; }
4445
-
4446
- .btn-aa {
4447
- display: inline-flex;
4448
- align-items: center;
4449
- justify-content: center;
4450
- width: 32px;
4451
- height: 32px;
4452
- border: none;
4453
- border-radius: 4px;
4454
- background: transparent;
4455
- color: var(--text-secondary);
4456
- cursor: pointer;
4457
- font-size: 14px;
4458
- font-weight: 700;
4459
- font-family: inherit;
4460
- transition: all 0.08s;
4461
- flex-shrink: 0;
4462
- }
4463
- .btn-aa:hover { background: var(--surface-raised); color: var(--text); }
4464
- .btn-aa.active { background: var(--toolbar-active); color: var(--blue-link); }
4465
-
4466
- .btn-source {
4467
- background: transparent;
4468
- color: var(--text-muted);
4469
- font-size: 11px;
4470
- font-weight: 600;
4471
- padding: 3px 6px;
4472
- border-radius: 4px;
4473
- border: 1px solid transparent;
4474
- cursor: pointer;
4475
- font-family: Monaco, Menlo, Consolas, monospace;
4476
- }
4477
- .btn-source:hover { color: var(--text-secondary); border-color: var(--border); }
4478
- .btn-source.active { color: var(--blue-link); border-color: rgba(29,155,209,0.3); }
4479
-
4480
- /* ── Inline link popover (Slack-style two-field) ── */
4481
- .link-popover {
4482
- display: none;
4483
- position: absolute;
4484
- z-index: 30;
4485
- background: var(--surface-raised);
4486
- border: 1px solid var(--border);
4487
- border-radius: 8px;
4488
- padding: 12px 14px;
4489
- box-shadow: 0 4px 20px rgba(0,0,0,0.5);
4490
- flex-direction: column;
4491
- gap: 8px;
4492
- width: 340px;
4493
- }
4494
- .link-popover.visible { display: flex; }
4495
-
4496
- .link-popover label {
4497
- font-size: 12px;
4498
- font-weight: 600;
4499
- color: var(--text-secondary);
4500
- display: block;
4501
- margin-bottom: 3px;
4502
- }
4503
-
4504
- .link-popover input {
4505
- width: 100%;
4506
- background: var(--bg);
4507
- border: 1px solid var(--border);
4508
- border-radius: 4px;
4509
- padding: 6px 8px;
4510
- font-size: 13px;
4511
- color: var(--text);
4512
- font-family: inherit;
4513
- outline: none;
4514
- }
4515
- .link-popover input:focus { border-color: var(--border-focus); }
4516
- .link-popover input::placeholder { color: var(--text-muted); }
4517
-
4518
- .link-popover .popover-actions {
4519
- display: flex;
4520
- justify-content: flex-end;
4521
- gap: 6px;
4522
- margin-top: 2px;
4523
- }
4524
-
4525
- .link-popover .popover-btn {
4526
- border: none;
4527
- border-radius: 4px;
4528
- padding: 5px 12px;
4529
- font-size: 12px;
4530
- font-weight: 700;
4531
- cursor: pointer;
4532
- font-family: inherit;
4533
- }
4534
- .link-popover .popover-btn-save {
4535
- background: var(--green);
4536
- color: #fff;
4537
- }
4538
- .link-popover .popover-btn-save:hover { background: var(--green-hover); }
4539
- .link-popover .popover-btn-cancel {
4540
- background: transparent;
4541
- color: var(--text-muted);
4542
- }
4543
- .link-popover .popover-btn-cancel:hover { color: var(--text-secondary); }
4544
-
4545
- /* ── Result overlay ── */
4546
- .overlay {
4547
- display: none;
4548
- position: fixed;
4549
- inset: 0;
4550
- background: rgba(0,0,0,0.8);
4551
- z-index: 100;
4552
- align-items: center;
4553
- justify-content: center;
4554
- }
4555
- .overlay.visible { display: flex; }
4556
-
4557
- .overlay-msg {
4558
- text-align: center;
4559
- font-size: 18px;
4560
- font-weight: 600;
4561
- }
4562
- .overlay-msg.success { color: #2eb67d; }
4563
- .overlay-msg.error { color: var(--red); }
4564
- .overlay-msg small {
4565
- display: block;
4566
- margin-top: 8px;
4567
- font-size: 13px;
4568
- font-weight: 400;
4569
- color: var(--text-muted);
4570
- }
4571
-
4572
- /* Scrollbar */
4573
- .editor::-webkit-scrollbar { width: 6px; }
4574
- .editor::-webkit-scrollbar-track { background: transparent; }
4575
- .editor::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
4576
- </style>
4577
- </head>
4578
- <body>
4579
-
4580
- <div class="page">
4581
- <!-- Branding -->
4582
- <div class="brand">
4583
- <div class="brand-name">Agent Slack</div>
4584
- <div class="brand-byline">By <a href="https://stably.ai" target="_blank" rel="noopener"><img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjM3OCIgaGVpZ2h0PSIyMzc4IiB2aWV3Qm94PSIwIDAgMjM3OCAyMzc4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDBfMTZfNDcpIj4KPHBhdGggZD0iTTExODguOTQgNjE4LjY0NUMxNTAwLjY5IDYxOC42NDUgMTc1My4zNiA4NzEuMzIyIDE3NTMuMzYgMTE4My4wNEMxNzUzLjM2IDE0OTQuNzIgMTUwMC42OSAxNzQ3LjM5IDExODguOTQgMTc0Ny4zOUM4NzcuMjUyIDE3NDcuMzkgNjI0LjU0NyAxNDk0LjcyIDYyNC41NDcgMTE4My4wNEM2MjQuNTQ3IDg3MS4zMTcgODc3LjI1MiA2MTguNjQ1IDExODguOTQgNjE4LjY0NVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xMTg4Ljk0IDBDMTA0MC44MSAwIDg5OS40NiAyOC40NTgxIDc2OC42MTkgNzguMTc0MkM3OTcuMjAyIDEwNy4yMDMgODIwLjg0IDE0MC45OTIgODM4Ljk5IDE3OC4wNTJDOTQ4Ljg2MSAxMzkuODE4IDEwNjYuMzUgMTE4LjA1NSAxMTg5IDExOC4wNTVDMTc3OS41IDExOC4wNTUgMjI1OS44NSA1OTguNTYxIDIyNTkuODUgMTE4OC45NEMyMjU5Ljg1IDE3NzkuMzEgMTc3OS41IDIyNTkuOTEgMTE4OC45NCAyMjU5LjkxQzU5OC41MDEgMjI1OS45MSAxMTguMTIgMTc3OS4zNyAxMTguMTIgMTE4OUMxMTguMTIgOTkwLjEwNiAxNzMuNjA3IDgwNC40MDggMjY4LjQwOSA2NDQuNjNDMjM1LjkxOSA2MTkuNzU5IDIwNy4yNzEgNTg5LjkzNyAxODQuMzI4IDU1NS45NThDNjguMjEzNiA3MzkuNDkzIDAgOTU2LjE4NiAwIDExODlDMCAxODQ0LjYgNTMzLjMzIDIzNzguMDEgMTE4OC45NCAyMzc4LjAxQzE4NDQuNiAyMzc4LjAxIDIzNzggMTg0NC42MSAyMzc4IDExODlDMjM3OCA1MzMuMzk1IDE4NDQuNiAwIDExODguOTQgMFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik00OTguNjI0IDExMS4xMUM2MzMuMjA5IDExMS4xMSA3NDIuMjg3IDIyMC4xODggNzQyLjI4NyAzNTQuNzczQzc0Mi4yODcgNDg5LjM1OCA2MzMuMjA5IDU5OC40MzYgNDk4LjYyNCA1OTguNDM2QzM2NC4wNzEgNTk4LjQzNiAyNTQuOTYxIDQ4OS4zNTggMjU0Ljk2MSAzNTQuNzczQzI1NC45NjEgMjIwLjE4OCAzNjQuMDcxIDExMS4xMSA0OTguNjI0IDExMS4xMVoiIGZpbGw9IndoaXRlIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfMTZfNDciPgo8cmVjdCB3aWR0aD0iMjM3OCIgaGVpZ2h0PSIyMzc4IiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=" alt="" /></a><a href="https://stably.ai" target="_blank" rel="noopener">Stably.ai</a></div>
4585
- <div class="brand-sub">draft mode</div>
4586
- </div>
4587
-
4588
- <!-- Context: channel + thread link -->
4589
- <div class="context-bar">
4590
- <svg class="hash-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
4591
- <line x1="7.5" y1="2" x2="6" y2="18"/><line x1="14" y1="2" x2="12.5" y2="18"/>
4592
- <line x1="3" y1="7" x2="17.5" y2="7"/><line x1="2.5" y1="13" x2="17" y2="13"/>
4593
- </svg>
4594
- <span class="channel" id="channelName"></span>
4595
- <span class="workspace-label" id="workspaceName"></span>
4596
- <a class="thread-link" id="threadLink" style="display:none" target="_blank">
4597
- <span>View thread</span>
4598
- <svg viewBox="0 0 16 16" stroke-linecap="round" stroke-linejoin="round">
4599
- <path d="M6 3h7v7"/><path d="M13 3L6 10"/>
4600
- </svg>
4601
- </a>
4602
- </div>
4603
-
4604
- <!-- Composer -->
4605
- <div class="composer">
4606
- <div class="toolbar" id="toolbar">
4607
- <button class="toolbar-btn" data-cmd="bold" data-tip="Bold (&#8984;B)"><span class="b">B</span></button>
4608
- <button class="toolbar-btn" data-cmd="italic" data-tip="Italic (&#8984;I)"><span class="i">I</span></button>
4609
- <button class="toolbar-btn" data-cmd="strikethrough" data-tip="Strikethrough (&#8984;&#8679;X)"><span class="s">S</span></button>
4610
- <div class="toolbar-sep"></div>
4611
- <button class="toolbar-btn" data-cmd="link" data-tip="Link (&#8984;K)">
4612
- <svg viewBox="0 0 16 16"><path d="M6.354 5.5H4a3 3 0 0 0 0 6h3a3 3 0 0 0 2.83-4M9.646 10.5H12a3 3 0 0 0 0-6H9a3 3 0 0 0-2.83 4" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
4613
- </button>
4614
- <div class="toolbar-sep"></div>
4615
- <button class="toolbar-btn" data-cmd="insertOrderedList" data-tip="Numbered list (&#8984;&#8679;7)">
4616
- <svg viewBox="0 0 16 16"><text x="1" y="5" font-size="5" fill="currentColor" font-family="system-ui" font-weight="600">1.</text><line x1="7" y1="3.5" x2="15" y2="3.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><text x="1" y="10" font-size="5" fill="currentColor" font-family="system-ui" font-weight="600">2.</text><line x1="7" y1="8.5" x2="15" y2="8.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><text x="1" y="15" font-size="5" fill="currentColor" font-family="system-ui" font-weight="600">3.</text><line x1="7" y1="13.5" x2="15" y2="13.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
4617
- </button>
4618
- <button class="toolbar-btn" data-cmd="insertUnorderedList" data-tip="Bulleted list (&#8984;&#8679;8)">
4619
- <svg viewBox="0 0 16 16"><circle cx="3" cy="3.5" r="1.5" fill="currentColor"/><line x1="7" y1="3.5" x2="15" y2="3.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><circle cx="3" cy="8.5" r="1.5" fill="currentColor"/><line x1="7" y1="8.5" x2="15" y2="8.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><circle cx="3" cy="13.5" r="1.5" fill="currentColor"/><line x1="7" y1="13.5" x2="15" y2="13.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
4620
- </button>
4621
- <div class="toolbar-sep"></div>
4622
- <button class="toolbar-btn" data-cmd="blockquote" data-tip="Quote (&#8984;&#8679;9)">
4623
- <svg viewBox="0 0 16 16"><rect x="1" y="2" width="2.5" height="12" rx="1" fill="currentColor" opacity="0.5"/><line x1="6" y1="4" x2="15" y2="4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><line x1="6" y1="8" x2="13" y2="8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><line x1="6" y1="12" x2="11" y2="12" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
4624
- </button>
4625
- <div class="toolbar-sep"></div>
4626
- <button class="toolbar-btn" data-cmd="code" data-tip="Code (&#8984;E)">
4627
- <svg viewBox="0 0 16 16"><polyline points="5,3 1,8 5,13" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/><polyline points="11,3 15,8 11,13" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
4628
- </button>
4629
- <button class="toolbar-btn" data-cmd="codeblock" data-tip="Code block (&#8984;&#8679;C)">
4630
- <svg viewBox="0 0 16 16"><rect x="1" y="1" width="14" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="1.2"/><polyline points="5,5 3,8 5,11" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/><polyline points="11,5 13,8 11,11" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/></svg>
4631
- </button>
4632
- </div>
4633
-
4634
- <div
4635
- class="editor"
4636
- id="editor"
4637
- contenteditable="true"
4638
- data-placeholder="Message #channel"
4639
- spellcheck="true"
4640
- ></div>
4641
-
4642
- <textarea class="source-editor" id="sourceEditor" spellcheck="false"></textarea>
4643
-
4644
- <div class="bottom-bar">
4645
- <div class="bottom-left">
4646
- <button class="btn-aa active" id="aaToggle" onclick="toggleToolbar()" data-tip="Formatting">Aa</button>
4647
- <button class="btn-source" id="sourceToggle" onclick="toggleSourceMode()">mrkdwn</button>
4648
- <span class="hint"><kbd id="modKey">&#8984;</kbd><kbd>Enter</kbd> to send</span>
4649
- </div>
4650
- <div style="display:flex;align-items:center;gap:8px;">
4651
- <button class="cancel-link" onclick="handleCancel()">Cancel</button>
4652
- <button class="btn-send" id="sendBtn" onclick="handleSend()" disabled>
4653
- <svg viewBox="0 0 20 20"><path d="M1.7 9.1l7.3 1 .01 0L1.7 9.1zm0 1.8l7.3-1-7.3 1zM1.5 2.1c-.2-.7.5-1.3 1.1-1L19 8.9c.6.3.6 1.2 0 1.5L2.6 18.9c-.7.3-1.3-.3-1.1-1l1.8-6.9L11 10 3.3 8.9 1.5 2.1z"/></svg>
4654
- </button>
4655
- </div>
4656
- </div>
4657
- </div>
4658
- </div>
4659
-
4660
- <!-- Inline link popover (Slack-style two-field) -->
4661
- <div class="link-popover" id="linkPopover">
4662
- <div>
4663
- <label>Text</label>
4664
- <input type="text" id="linkTextInput" placeholder="Display text">
4665
- </div>
4666
- <div>
4667
- <label>Link</label>
4668
- <input type="text" id="linkUrlInput" placeholder="https://example.com">
4669
- </div>
4670
- <div class="popover-actions">
4671
- <button class="popover-btn popover-btn-cancel" onclick="closeLinkPopover()">Cancel</button>
4672
- <button class="popover-btn popover-btn-save" onclick="applyLink()">Save</button>
4673
- </div>
4674
- </div>
4675
-
4676
- <!-- Result overlay -->
4677
- <div class="overlay" id="resultOverlay">
4678
- <div class="overlay-msg" id="resultMsg"></div>
4679
- </div>
4680
-
4681
- <script>
4682
- // ─── Config ───
4683
- const CONFIG = __DRAFT_CONFIG__;
4684
- const IS_MAC = /Mac|iPhone/.test(navigator.platform);
4685
- const MOD = IS_MAC ? 'metaKey' : 'ctrlKey';
4686
-
4687
- // ─── Init header ───
4688
- document.getElementById('channelName').textContent = CONFIG.channelName;
4689
- if (CONFIG.workspaceName) {
4690
- document.getElementById('workspaceName').textContent = CONFIG.workspaceName;
4691
- }
4692
- if (!IS_MAC) {
4693
- document.getElementById('modKey').textContent = 'Ctrl';
4694
- document.querySelectorAll('[data-tip]').forEach(el => {
4695
- el.dataset.tip = el.dataset.tip.replace(/\\u2318/g, 'Ctrl+').replace(/\\u21E7/g, 'Shift+');
4696
- });
4697
- }
4698
- if (CONFIG.threadTs) {
4699
- const link = document.getElementById('threadLink');
4700
- link.style.display = '';
4701
- if (CONFIG.threadUrl) {
4702
- link.href = CONFIG.threadUrl;
4703
- } else {
4704
- link.removeAttribute('href');
4705
- link.style.cursor = 'default';
4706
- link.style.color = 'var(--text-muted)';
4707
- }
4708
- }
4709
-
4710
- // ─── Editor refs ───
4711
- const editor = document.getElementById('editor');
4712
- const sourceEditor = document.getElementById('sourceEditor');
4713
- const linkPopover = document.getElementById('linkPopover');
4714
- const linkUrlInput = document.getElementById('linkUrlInput');
4715
- let sourceMode = false;
4716
-
4717
- // Set Slack-style placeholder
4718
- editor.dataset.placeholder = 'Message #' + CONFIG.channelName;
4719
-
4720
- // ─── Detect if cursor is inside a code element ───
4721
- function isInsideCode(node) {
4722
- let el = node;
4723
- while (el && el !== editor) {
4724
- if (el.nodeType === 1) {
4725
- const tag = el.tagName.toLowerCase();
4726
- if (tag === 'pre' || tag === 'code') { return true; }
4727
- }
4728
- el = el.parentNode;
4729
- }
4730
- return false;
4731
- }
4732
-
4733
- function cursorInCode() {
4734
- const sel = window.getSelection();
4735
- if (!sel || sel.rangeCount === 0) { return false; }
4736
- return isInsideCode(sel.anchorNode);
4737
- }
4738
-
4739
- // ─── mrkdwn to HTML (initial content) ───
4740
- function escapeHtml(t) {
4741
- return t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
4742
- }
4743
-
4744
- function mrkdwnToHtml(text) {
4745
- if (!text) { return ''; }
4746
- const codeBlocks = [];
4747
- text = text.replace(/\`\`\`([\\s\\S]*?)\`\`\`/g, (_, code) => {
4748
- codeBlocks.push('<pre>' + escapeHtml(code.trim()) + '</pre>');
4749
- return '\\x00CB' + (codeBlocks.length - 1) + '\\x00';
4750
- });
4751
- const inlineCodes = [];
4752
- text = text.replace(/\`([^\`\\n]+)\`/g, (_, code) => {
4753
- inlineCodes.push('<code>' + escapeHtml(code) + '</code>');
4754
- return '\\x00IC' + (inlineCodes.length - 1) + '\\x00';
4755
- });
4756
-
4757
- const lines = text.split('\\n');
4758
- let html = '', idx = 0;
4759
- while (idx < lines.length) {
4760
- const line = lines[idx];
4761
- if (line.startsWith('> ')) {
4762
- const q = [];
4763
- while (idx < lines.length && lines[idx].startsWith('> ')) { q.push(fmtInline(lines[idx].slice(2))); idx++; }
4764
- html += '<blockquote>' + q.join('<br>') + '</blockquote>'; continue;
4765
- }
4766
- if (/^[\\u2022\\-\\*] /.test(line)) {
4767
- const items = [];
4768
- while (idx < lines.length && /^[\\u2022\\-\\*] /.test(lines[idx])) { items.push('<li>' + fmtInline(lines[idx].replace(/^[\\u2022\\-\\*] /, '')) + '</li>'); idx++; }
4769
- html += '<ul>' + items.join('') + '</ul>'; continue;
4770
- }
4771
- if (/^\\d+[\\.\\)] /.test(line)) {
4772
- const items = [];
4773
- while (idx < lines.length && /^\\d+[\\.\\)] /.test(lines[idx])) { items.push('<li>' + fmtInline(lines[idx].replace(/^\\d+[\\.\\)] /, '')) + '</li>'); idx++; }
4774
- html += '<ol>' + items.join('') + '</ol>'; continue;
4775
- }
4776
- html += (line ? fmtInline(line) : '') + '<br>';
4777
- idx++;
4778
- }
4779
- codeBlocks.forEach((b, i) => { html = html.replace('\\x00CB' + i + '\\x00', b); });
4780
- inlineCodes.forEach((c, i) => { html = html.replace('\\x00IC' + i + '\\x00', c); });
4781
- return html.replace(/(<br>)+$/, '');
4782
- }
4783
-
4784
- function fmtInline(t) {
4785
- t = t.replace(/<(https?:\\/\\/[^|>]+)\\|([^>]+)>/g, '<a href="$1" target="_blank">$2</a>');
4786
- t = t.replace(/<(https?:\\/\\/[^>]+)>/g, '<a href="$1" target="_blank">$1</a>');
4787
- t = t.replace(/\\*([^\\*]+)\\*/g, '<b>$1</b>');
4788
- t = t.replace(/(?<![a-zA-Z0-9])_([^_]+)_(?![a-zA-Z0-9])/g, '<i>$1</i>');
4789
- t = t.replace(/~([^~]+)~/g, '<s>$1</s>');
4790
- return t;
4791
- }
4792
-
4793
- // ─── HTML to mrkdwn (submission) ───
4794
- function htmlToMrkdwn(root) {
4795
- const visited = new Set();
4796
- function walk(node) {
4797
- if (visited.has(node)) { return ''; }
4798
- if (node.nodeType === 3) { return node.textContent || ''; }
4799
- if (node.nodeType !== 1) { return ''; }
4800
- const el = node;
4801
- const tag = el.tagName.toLowerCase();
4802
- const kids = () => Array.from(el.childNodes).map(walk).join('');
4803
- switch (tag) {
4804
- case 'b': case 'strong': { const c = kids(); return c.trim() ? '*' + c + '*' : c; }
4805
- case 'i': case 'em': { const c = kids(); return c.trim() ? '_' + c + '_' : c; }
4806
- case 's': case 'strike': case 'del': { const c = kids(); return c.trim() ? '~' + c + '~' : c; }
4807
- case 'code': return '\`' + (el.textContent || '') + '\`';
4808
- case 'pre': {
4809
- // Collect adjacent pres into one code block
4810
- const lines = [(el.textContent || '').trimEnd()];
4811
- let next = el.nextElementSibling;
4812
- while (next && next.tagName === 'PRE') {
4813
- lines.push((next.textContent || '').trimEnd());
4814
- visited.add(next);
4815
- next = next.nextElementSibling;
4816
- }
4817
- return '\`\`\`\\n' + lines.filter(l => l).join('\\n') + '\\n\`\`\`\\n';
4818
- }
4819
- case 'blockquote': { const c = kids().trim(); return c.split('\\n').map(l => '> ' + l).join('\\n') + '\\n'; }
4820
- case 'ul': { let r = ''; for (const li of el.querySelectorAll(':scope > li')) { r += '\\u2022 ' + walk(li).trim() + '\\n'; } return r; }
4821
- case 'ol': { let r = '', n = 1; for (const li of el.querySelectorAll(':scope > li')) { r += n + '. ' + walk(li).trim() + '\\n'; n++; } return r; }
4822
- case 'li': return kids();
4823
- case 'a': { const h = el.getAttribute('href'); const t = kids(); return (h && t && h !== t) ? '<' + h + '|' + t + '>' : (h || t); }
4824
- case 'br': return '\\n';
4825
- case 'div': case 'p': {
4826
- const c = kids();
4827
- const p = el.parentElement;
4828
- if (p && ['li','blockquote','td'].includes(p.tagName.toLowerCase())) { return c; }
4829
- return c.endsWith('\\n') ? c : c + '\\n';
4830
- }
4831
- case 'span': {
4832
- const st = el.style;
4833
- let c = kids();
4834
- if (st.fontWeight === 'bold' || Number(st.fontWeight) >= 700) { c = c.trim() ? '*' + c + '*' : c; }
4835
- if (st.fontStyle === 'italic') { c = c.trim() ? '_' + c + '_' : c; }
4836
- if (st.textDecoration && st.textDecoration.includes('line-through')) { c = c.trim() ? '~' + c + '~' : c; }
4837
- return c;
4838
- }
4839
- default: return kids();
4127
+ channels: [target.channel]
4128
+ });
4129
+ const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
4130
+ const channelId = await resolveChannelId(client, target.channel);
4131
+ const name = normalizeSlackReactionName(input.emoji);
4132
+ await client.api(`reactions.${input.action}`, {
4133
+ channel: channelId,
4134
+ timestamp: ts,
4135
+ name
4136
+ });
4840
4137
  }
4841
- }
4842
- return walk(root).replace(/\\n{3,}/g, '\\n\\n').trim();
4138
+ });
4139
+ return { ok: true };
4843
4140
  }
4844
4141
 
4845
- // ─── Init editor content ───
4846
- if (CONFIG.initialText) { editor.innerHTML = mrkdwnToHtml(CONFIG.initialText); }
4847
-
4848
- // ─── Toolbar execution ───
4849
- function execCmd(command) {
4850
- // Block formatting inside code (except toggling code off)
4851
- const inCode = cursorInCode();
4852
- if (inCode && command !== 'code' && command !== 'codeblock') { return; }
4853
-
4854
- switch (command) {
4855
- case 'bold':
4856
- case 'italic':
4857
- case 'strikethrough':
4858
- case 'insertOrderedList':
4859
- case 'insertUnorderedList':
4860
- document.execCommand(command, false, null);
4861
- break;
4862
- case 'blockquote':
4863
- document.execCommand('formatBlock', false, 'blockquote');
4864
- break;
4865
- case 'code': {
4866
- const sel = window.getSelection();
4867
- if (!sel || sel.rangeCount === 0) { break; }
4868
- // If already in code, unwrap
4869
- if (inCode) {
4870
- const codeEl = sel.anchorNode.nodeType === 1 ? sel.anchorNode : sel.anchorNode.parentElement;
4871
- const code = codeEl.closest('code');
4872
- if (code) {
4873
- const text = document.createTextNode(code.textContent || '');
4874
- code.parentNode.replaceChild(text, code);
4875
- const r = document.createRange(); r.selectNodeContents(text); sel.removeAllRanges(); sel.addRange(r);
4142
+ // src/cli/draft-server.ts
4143
+ import { createServer } from "node:http";
4144
+ import { execFile } from "node:child_process";
4145
+ import { readFileSync as readFileSync2 } from "node:fs";
4146
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
4147
+ import { dirname as dirname3, join as join12 } from "node:path";
4148
+ function openDraftEditor(config) {
4149
+ return new Promise((resolve3, reject) => {
4150
+ let settled = false;
4151
+ const server = createServer(async (req, res) => {
4152
+ if (req.method === "GET" && (req.url === "/" || req.url === "/index.html")) {
4153
+ const html = buildEditorHtml(config);
4154
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
4155
+ res.end(html);
4156
+ return;
4157
+ }
4158
+ if (req.method === "POST" && req.url === "/send") {
4159
+ try {
4160
+ const body = await readBody(req);
4161
+ const data = JSON.parse(body);
4162
+ if (typeof data.text !== "string" || !data.text.trim()) {
4163
+ res.writeHead(400, { "Content-Type": "application/json" });
4164
+ res.end(JSON.stringify({ ok: false, error: "text is required" }));
4165
+ return;
4166
+ }
4167
+ const sendResult = await config.onSend(data.text);
4168
+ res.writeHead(200, { "Content-Type": "application/json" });
4169
+ res.end(JSON.stringify({ ok: true, ts: sendResult.ts }));
4170
+ settled = true;
4171
+ resolve3({ sent: true, text: data.text });
4172
+ setTimeout(() => server.close(), 300);
4173
+ } catch (err) {
4174
+ const safeMessage = err instanceof Error ? err.message.replace(/xox[a-z]-[A-Za-z0-9-]+/g, "[REDACTED]") : "Send failed";
4175
+ res.writeHead(500, { "Content-Type": "application/json" });
4176
+ res.end(JSON.stringify({ ok: false, error: safeMessage }));
4876
4177
  }
4877
- break;
4178
+ return;
4878
4179
  }
4879
- const range = sel.getRangeAt(0);
4880
- if (range.collapsed) { break; }
4881
- const code = document.createElement('code');
4882
- try { range.surroundContents(code); } catch (_) {
4883
- const frag = range.extractContents(); code.appendChild(frag); range.insertNode(code);
4180
+ if (req.method === "POST" && req.url === "/cancel") {
4181
+ res.writeHead(200, { "Content-Type": "application/json" });
4182
+ res.end(JSON.stringify({ ok: true }));
4183
+ settled = true;
4184
+ resolve3({ cancelled: true });
4185
+ setTimeout(() => server.close(), 300);
4186
+ return;
4884
4187
  }
4885
- sel.removeAllRanges();
4886
- const nr = document.createRange(); nr.selectNodeContents(code); sel.addRange(nr);
4887
- break;
4888
- }
4889
- case 'codeblock': {
4890
- if (inCode) { break; } // No nesting
4891
- const sel = window.getSelection();
4892
- if (!sel) { break; }
4893
- const pre = document.createElement('pre');
4894
- if (!sel.isCollapsed) {
4895
- const range = sel.getRangeAt(0);
4896
- pre.textContent = range.extractContents().textContent || '';
4897
- range.insertNode(pre);
4898
- } else {
4899
- pre.innerHTML = '<br>';
4900
- const range = sel.getRangeAt(0);
4901
- range.insertNode(pre);
4902
- const nr = document.createRange(); nr.setStart(pre, 0); nr.collapse(true);
4903
- sel.removeAllRanges(); sel.addRange(nr);
4188
+ res.writeHead(404);
4189
+ res.end("Not found");
4190
+ });
4191
+ server.on("error", (err) => {
4192
+ if (!settled) {
4193
+ reject(err);
4904
4194
  }
4905
- break;
4906
- }
4907
- case 'link':
4908
- openLinkPopover();
4909
- return; // Don't refocus — popover takes focus
4910
- }
4911
- editor.focus();
4912
- updateToolbarState();
4913
- }
4914
-
4915
- // Toolbar button clicks
4916
- document.querySelectorAll('.toolbar-btn[data-cmd]').forEach(btn => {
4917
- btn.addEventListener('mousedown', (e) => {
4918
- e.preventDefault();
4919
- execCmd(btn.dataset.cmd);
4920
- });
4921
- });
4922
-
4923
- // ─── Toolbar state ───
4924
- function updateToolbarState() {
4925
- const inCode = cursorInCode();
4926
- document.querySelectorAll('.toolbar-btn[data-cmd]').forEach(btn => {
4927
- const cmd = btn.dataset.cmd;
4928
- // Disable non-code buttons when inside code
4929
- if (inCode && cmd !== 'code' && cmd !== 'codeblock') {
4930
- btn.classList.add('disabled');
4931
- btn.classList.remove('active');
4932
- return;
4933
- }
4934
- btn.classList.remove('disabled');
4935
- let active = false;
4936
- try {
4937
- if (['bold','italic','strikethrough','insertOrderedList','insertUnorderedList'].includes(cmd)) {
4938
- active = document.queryCommandState(cmd);
4195
+ });
4196
+ server.on("close", () => {
4197
+ clearTimeout(idleTimeout);
4198
+ if (!settled) {
4199
+ settled = true;
4200
+ resolve3({ cancelled: true });
4201
+ }
4202
+ });
4203
+ const idleTimeout = setTimeout(() => {
4204
+ if (!settled) {
4205
+ server.close();
4939
4206
  }
4940
- if (cmd === 'code') { active = inCode; }
4941
- } catch (_) {}
4942
- btn.classList.toggle('active', active);
4207
+ }, 30 * 60 * 1000);
4208
+ server.listen(0, "127.0.0.1", () => {
4209
+ const addr = server.address();
4210
+ const port = typeof addr === "object" && addr ? addr.port : 0;
4211
+ const url = `http://127.0.0.1:${port}`;
4212
+ process.stderr.write(`Draft editor: ${url}
4213
+ `);
4214
+ openBrowser(url);
4215
+ });
4943
4216
  });
4944
4217
  }
4945
-
4946
- editor.addEventListener('keyup', updateToolbarState);
4947
- editor.addEventListener('mouseup', updateToolbarState);
4948
- editor.addEventListener('focus', updateToolbarState);
4949
-
4950
- // ─── Keyboard shortcuts (Slack-identical) ───
4951
- editor.addEventListener('keydown', (e) => {
4952
- const mod = e[MOD];
4953
-
4954
- // Cmd+Enter Send
4955
- if (mod && e.key === 'Enter') { e.preventDefault(); handleSend(); return; }
4956
-
4957
- // Cmd+B → Bold
4958
- if (mod && !e.shiftKey && e.key === 'b') { e.preventDefault(); execCmd('bold'); return; }
4959
-
4960
- // Cmd+I → Italic
4961
- if (mod && !e.shiftKey && e.key === 'i') { e.preventDefault(); execCmd('italic'); return; }
4962
-
4963
- // Cmd+Shift+X → Strikethrough
4964
- if (mod && e.shiftKey && (e.key === 'X' || e.key === 'x')) { e.preventDefault(); execCmd('strikethrough'); return; }
4965
-
4966
- // Cmd+E → Inline code (Slack's actual shortcut)
4967
- if (mod && !e.shiftKey && e.key === 'e') { e.preventDefault(); execCmd('code'); return; }
4968
-
4969
- // Cmd+Shift+C → Code block
4970
- if (mod && e.shiftKey && (e.key === 'C' || e.key === 'c' || e.code === 'KeyC')) { e.preventDefault(); execCmd('codeblock'); return; }
4971
-
4972
- // Cmd+K → Link
4973
- if (mod && !e.shiftKey && e.key === 'k') { e.preventDefault(); execCmd('link'); return; }
4974
-
4975
- // Cmd+Shift+7 → Numbered list
4976
- if (mod && e.shiftKey && e.key === '7') { e.preventDefault(); execCmd('insertOrderedList'); return; }
4977
-
4978
- // Cmd+Shift+8 → Bulleted list
4979
- if (mod && e.shiftKey && e.key === '8') { e.preventDefault(); execCmd('insertUnorderedList'); return; }
4980
-
4981
- // Cmd+Shift+9 → Blockquote
4982
- if (mod && e.shiftKey && e.key === '9') { e.preventDefault(); execCmd('blockquote'); return; }
4983
- });
4984
-
4985
- // ─── Code block: merge split <pre> elements + escape logic ───
4986
- function findParentPre(node) {
4987
- while (node && node !== editor) {
4988
- if (node.nodeType === 1 && node.tagName === 'PRE') { return node; }
4989
- node = node.parentNode;
4990
- }
4991
- return null;
4992
- }
4993
-
4994
- function exitCodeBlock(pre) {
4995
- const sel = window.getSelection();
4996
- const p = document.createElement('div');
4997
- p.innerHTML = '<br>';
4998
- if (pre.parentNode === editor) {
4999
- pre.parentNode.insertBefore(p, pre.nextSibling);
5000
- } else {
5001
- const wrapper = pre.parentNode;
5002
- wrapper.parentNode.insertBefore(p, wrapper.nextSibling);
5003
- }
5004
- const nr = document.createRange();
5005
- nr.setStart(p, 0);
5006
- nr.collapse(true);
5007
- sel.removeAllRanges();
5008
- sel.addRange(nr);
5009
- }
5010
-
5011
- // Code block management: the browser splits <pre> on Enter, creating adjacent
5012
- // <pre> elements. We let this happen and handle it via:
5013
- // 1. CSS: adjacent pres look like one block (no gaps/borders between them)
5014
- // 2. Serialization: htmlToMrkdwn treats adjacent pres as one code block
5015
- // 3. Escape: when user creates an empty pre after another empty pre, exit code
5016
-
5017
- // Track code block escape (double-Enter on empty line)
5018
- editor.addEventListener('input', () => {
5019
- const pres = Array.from(editor.querySelectorAll('pre'));
5020
- if (pres.length < 2) { return; }
5021
-
5022
- // Check for escape: last two adjacent pres are both empty
5023
- for (let i = pres.length - 1; i > 0; i--) {
5024
- const cur = pres[i];
5025
- const prev = pres[i - 1];
5026
- const curEmpty = !cur.textContent?.trim();
5027
- const prevEmpty = !prev.textContent?.trim();
5028
-
5029
- if (curEmpty && prevEmpty) {
5030
- // Double-Enter escape: remove both empty pres, place cursor after
5031
- const lastRealPre = pres[i - 2] || null;
5032
- // Remove the two empty pres and their wrappers
5033
- [cur, prev].forEach(p => {
5034
- const parent = p.parentNode;
5035
- p.remove();
5036
- if (parent && parent !== editor && parent.tagName === 'DIV' && !parent.textContent?.trim()) {
5037
- parent.remove();
5038
- }
5039
- });
5040
- if (lastRealPre) {
5041
- exitCodeBlock(lastRealPre);
5042
- } else {
5043
- // No code left — just place cursor
5044
- const sel = window.getSelection();
5045
- if (sel) {
5046
- const div = document.createElement('div');
5047
- div.innerHTML = '<br>';
5048
- editor.appendChild(div);
5049
- const r = document.createRange();
5050
- r.setStart(div, 0);
5051
- r.collapse(true);
5052
- sel.removeAllRanges();
5053
- sel.addRange(r);
5054
- }
4218
+ var MAX_BODY_BYTES = 1024 * 1024;
4219
+ function readBody(req) {
4220
+ return new Promise((resolve3, reject) => {
4221
+ const chunks = [];
4222
+ let totalBytes = 0;
4223
+ req.on("data", (chunk) => {
4224
+ totalBytes += chunk.length;
4225
+ if (totalBytes > MAX_BODY_BYTES) {
4226
+ req.destroy();
4227
+ reject(new Error("Request body too large"));
4228
+ return;
5055
4229
  }
5056
- return;
5057
- }
5058
- }
5059
- });
5060
-
5061
- // ArrowDown at end of last code block → exit
5062
- editor.addEventListener('keydown', (e) => {
5063
- if (e.key === 'ArrowDown') {
5064
- const sel = window.getSelection();
5065
- if (!sel || sel.rangeCount === 0) { return; }
5066
- const pre = findParentPre(sel.anchorNode);
5067
- if (pre && !pre.nextElementSibling) {
5068
- e.preventDefault();
5069
- exitCodeBlock(pre);
5070
- }
5071
- }
5072
- }, true);
5073
-
5074
- // ─── Paste handler ───
5075
- editor.addEventListener('paste', (e) => {
5076
- const cd = e.clipboardData;
5077
- if (!cd) { return; }
5078
- const html = cd.getData('text/html');
5079
- if (html) {
5080
- e.preventDefault();
5081
- const tmp = document.createElement('div');
5082
- tmp.innerHTML = html;
5083
- tmp.querySelectorAll('script,style,meta,link').forEach(el => el.remove());
5084
- tmp.querySelectorAll('[style]').forEach(el => {
5085
- const s = el.style;
5086
- const keep = {};
5087
- if (s.fontWeight === 'bold' || Number(s.fontWeight) >= 700) { keep.fontWeight = s.fontWeight; }
5088
- if (s.fontStyle === 'italic') { keep.fontStyle = s.fontStyle; }
5089
- if (s.textDecoration?.includes('line-through')) { keep.textDecoration = 'line-through'; }
5090
- el.removeAttribute('style');
5091
- Object.assign(el.style, keep);
4230
+ chunks.push(chunk);
5092
4231
  });
5093
- document.execCommand('insertHTML', false, tmp.innerHTML);
5094
- }
5095
- });
5096
-
5097
- // ─── Link popover (Slack-style: select text, Cmd+K, two-field form) ───
5098
- let linkSavedRange = null;
5099
- let linkPopoverOpen = false;
5100
- const linkTextInput = document.getElementById('linkTextInput');
5101
-
5102
- function openLinkPopover() {
5103
- const sel = window.getSelection();
5104
- if (!sel || sel.rangeCount === 0) { return; }
5105
-
5106
- linkSavedRange = sel.getRangeAt(0).cloneRange();
5107
- const selectedText = sel.toString();
5108
-
5109
- // Position near selection
5110
- const rect = linkSavedRange.getBoundingClientRect();
5111
- const editorRect = editor.getBoundingClientRect();
5112
- linkPopover.style.top = (rect.bottom + window.scrollY + 6) + 'px';
5113
- linkPopover.style.left = Math.max(8, Math.min(rect.left + window.scrollX, editorRect.right - 360)) + 'px';
5114
-
5115
- // Pre-fill text field with selected text
5116
- linkTextInput.value = selectedText;
5117
-
5118
- // If selection is already a link, pre-fill URL
5119
- let existingUrl = '';
5120
- const anchor = sel.anchorNode?.parentElement?.closest('a');
5121
- if (anchor) {
5122
- existingUrl = anchor.getAttribute('href') || '';
5123
- linkTextInput.value = anchor.textContent || selectedText;
5124
- }
5125
- linkUrlInput.value = existingUrl;
5126
-
5127
- // Show popover (use rAF to avoid same-frame mousedown closing it)
5128
- requestAnimationFrame(() => {
5129
- linkPopover.classList.add('visible');
5130
- linkPopoverOpen = true;
5131
- linkUrlInput.focus();
5132
- linkUrlInput.select();
4232
+ req.on("end", () => resolve3(Buffer.concat(chunks).toString()));
4233
+ req.on("error", reject);
5133
4234
  });
5134
4235
  }
5135
-
5136
- function closeLinkPopover() {
5137
- linkPopover.classList.remove('visible');
5138
- linkPopoverOpen = false;
5139
- editor.focus();
5140
- if (linkSavedRange) {
5141
- const sel = window.getSelection();
5142
- sel.removeAllRanges();
5143
- sel.addRange(linkSavedRange);
5144
- }
5145
- }
5146
-
5147
- function applyLink() {
5148
- const url = linkUrlInput.value.trim();
5149
- const text = linkTextInput.value.trim();
5150
- linkPopover.classList.remove('visible');
5151
- linkPopoverOpen = false;
5152
-
5153
- if (!url || !linkSavedRange) { editor.focus(); return; }
5154
-
5155
- const sel = window.getSelection();
5156
- sel.removeAllRanges();
5157
- sel.addRange(linkSavedRange);
5158
-
5159
- // Check if we're updating an existing link
5160
- const anchor = sel.anchorNode?.parentElement?.closest('a');
5161
- if (anchor) {
5162
- anchor.setAttribute('href', url);
5163
- if (text) { anchor.textContent = text; }
5164
- } else {
5165
- // Create new link
5166
- const a = document.createElement('a');
5167
- a.href = url;
5168
- a.target = '_blank';
5169
- a.textContent = text || url;
5170
-
5171
- if (!linkSavedRange.collapsed) {
5172
- linkSavedRange.deleteContents();
5173
- }
5174
- linkSavedRange.insertNode(a);
5175
-
5176
- const nr = document.createRange();
5177
- nr.setStartAfter(a);
5178
- nr.collapse(true);
5179
- sel.removeAllRanges();
5180
- sel.addRange(nr);
5181
- }
5182
-
5183
- editor.focus();
5184
- linkSavedRange = null;
4236
+ function openBrowser(url) {
4237
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
4238
+ execFile(cmd, [url], () => {});
5185
4239
  }
5186
-
5187
- // Popover key handling both inputs
5188
- [linkTextInput, linkUrlInput].forEach(input => {
5189
- input.addEventListener('keydown', (e) => {
5190
- if (e.key === 'Enter') { e.preventDefault(); applyLink(); }
5191
- if (e.key === 'Escape') { e.preventDefault(); closeLinkPopover(); }
5192
- });
5193
- });
5194
-
5195
- // Close popover on outside click (delayed check to avoid same-event close)
5196
- document.addEventListener('mousedown', (e) => {
5197
- if (linkPopoverOpen && !linkPopover.contains(e.target) && !e.target.closest('.toolbar-btn')) {
5198
- closeLinkPopover();
4240
+ function buildSlackThreadUrl(config) {
4241
+ if (!config.workspaceUrl || !config.channelId || !config.threadTs) {
4242
+ return null;
5199
4243
  }
5200
- });
5201
-
5202
- // ─── Toolbar toggle (Aa button) ───
5203
- let toolbarVisible = true;
5204
- function toggleToolbar() {
5205
- toolbarVisible = !toolbarVisible;
5206
- document.getElementById('toolbar').style.display = toolbarVisible ? '' : 'none';
5207
- document.getElementById('aaToggle').classList.toggle('active', toolbarVisible);
5208
- }
5209
-
5210
- // ─── Send button state ───
5211
- function updateSendBtn() {
5212
- const text = sourceMode ? sourceEditor.value.trim() : (editor.textContent || '').trim();
5213
- document.getElementById('sendBtn').disabled = !text;
4244
+ const tsNoDot = config.threadTs.replaceAll(".", "");
4245
+ return `${config.workspaceUrl.replace(/\/$/, "")}/archives/${config.channelId}/p${tsNoDot}`;
5214
4246
  }
5215
- editor.addEventListener('input', updateSendBtn);
5216
-
5217
- // ─── Source mode ───
5218
- function toggleSourceMode() {
5219
- sourceMode = !sourceMode;
5220
- document.getElementById('sourceToggle').classList.toggle('active', sourceMode);
5221
- if (sourceMode) {
5222
- sourceEditor.value = htmlToMrkdwn(editor);
5223
- editor.style.display = 'none';
5224
- sourceEditor.style.display = 'block';
5225
- document.getElementById('toolbar').style.display = 'none';
5226
- document.getElementById('aaToggle').style.display = 'none';
5227
- sourceEditor.focus();
5228
- } else {
5229
- editor.innerHTML = mrkdwnToHtml(sourceEditor.value);
5230
- sourceEditor.style.display = 'none';
5231
- editor.style.display = '';
5232
- document.getElementById('toolbar').style.display = toolbarVisible ? '' : 'none';
5233
- document.getElementById('aaToggle').style.display = '';
5234
- editor.focus();
4247
+ function extractWorkspaceName(url) {
4248
+ if (!url) {
4249
+ return null;
5235
4250
  }
5236
- updateSendBtn();
5237
- }
5238
-
5239
- sourceEditor.addEventListener('keydown', (e) => {
5240
- if (e[MOD] && e.key === 'Enter') { e.preventDefault(); handleSend(); }
5241
- });
5242
- sourceEditor.addEventListener('input', updateSendBtn);
5243
-
5244
- // ─── Send / Cancel ───
5245
- let sending = false;
5246
-
5247
- async function handleSend() {
5248
- if (sending) { return; }
5249
- const text = sourceMode ? sourceEditor.value.trim() : htmlToMrkdwn(editor);
5250
- if (!text) { editor.focus(); return; }
5251
-
5252
- sending = true;
5253
- const btn = document.getElementById('sendBtn');
5254
- btn.disabled = true;
5255
- btn.innerHTML = '\\u2026';
5256
-
5257
4251
  try {
5258
- const resp = await fetch('/send', {
5259
- method: 'POST',
5260
- headers: { 'Content-Type': 'application/json' },
5261
- body: JSON.stringify({ text }),
5262
- });
5263
- const data = await resp.json();
5264
- if (data.ok) {
5265
- let sub = 'You can close this tab.';
5266
- if (data.ts && CONFIG.workspaceUrl && CONFIG.channelId) {
5267
- const tsNoDot = data.ts.replace('.', '');
5268
- const baseUrl = CONFIG.workspaceUrl.replace(/\\/$/, '');
5269
- const msgUrl = baseUrl + '/archives/' + CONFIG.channelId + '/p' + tsNoDot;
5270
- sub = '<a href="' + msgUrl + '" target="_blank" style="color:var(--accent);text-decoration:none;">View in Slack \\u2197</a><br><span style="opacity:0.6;font-size:12px;">You can close this tab.</span>';
5271
- }
5272
- showOverlay('Message sent \\u2705', sub, 'success');
5273
- } else {
5274
- throw new Error(data.error || 'Send failed');
4252
+ const host = new URL(url).hostname;
4253
+ const parts = host.split(".");
4254
+ if (parts.length >= 3 && parts.at(-2) === "slack") {
4255
+ return parts.slice(0, -2).join(".");
5275
4256
  }
5276
- } catch (err) {
5277
- showOverlay('Failed to send', err.message, 'error');
5278
- sending = false;
5279
- btn.disabled = false;
5280
- btn.innerHTML = '<svg viewBox="0 0 20 20" style="width:16px;height:16px;fill:currentColor"><path d="M1.7 9.1l7.3 1 .01 0L1.7 9.1zm0 1.8l7.3-1-7.3 1zM1.5 2.1c-.2-.7.5-1.3 1.1-1L19 8.9c.6.3.6 1.2 0 1.5L2.6 18.9c-.7.3-1.3-.3-1.1-1l1.8-6.9L11 10 3.3 8.9 1.5 2.1z"/></svg>';
4257
+ return host;
4258
+ } catch {
4259
+ return null;
5281
4260
  }
5282
4261
  }
5283
-
5284
- async function handleCancel() {
5285
- try { await fetch('/cancel', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); } catch (_) {}
5286
- showOverlay('Draft cancelled', 'You can close this tab.', 'success');
4262
+ function buildEditorHtml(config) {
4263
+ const threadUrl = buildSlackThreadUrl(config);
4264
+ const workspaceName = extractWorkspaceName(config.workspaceUrl);
4265
+ const injectedConfig = JSON.stringify({
4266
+ channelName: config.channelName,
4267
+ channelId: config.channelId || null,
4268
+ workspaceUrl: config.workspaceUrl || null,
4269
+ workspaceName,
4270
+ threadTs: config.threadTs || null,
4271
+ threadUrl,
4272
+ initialText: config.initialText || ""
4273
+ });
4274
+ const safeConfig = injectedConfig.replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
4275
+ return getEditorHtml().replace("__DRAFT_CONFIG__", safeConfig);
5287
4276
  }
5288
-
5289
- function showOverlay(title, sub, type) {
5290
- const overlay = document.getElementById('resultOverlay');
5291
- const msg = document.getElementById('resultMsg');
5292
- msg.className = 'overlay-msg ' + type;
5293
- msg.innerHTML = title + '<small>' + sub + '</small>';
5294
- overlay.classList.add('visible');
4277
+ var _editorHtml;
4278
+ function getEditorHtml() {
4279
+ if (!_editorHtml) {
4280
+ _editorHtml = readFileSync2(join12(dirname3(fileURLToPath2(import.meta.url)), "draft-editor.html"), "utf-8");
4281
+ }
4282
+ return _editorHtml;
5295
4283
  }
5296
4284
 
5297
- // ─── Focus ───
5298
- editor.focus();
5299
- updateSendBtn();
5300
- </script>
5301
- </body>
5302
- </html>`;
5303
-
5304
4285
  // src/cli/draft-actions.ts
5305
4286
  async function draftMessage(input) {
5306
4287
  const target = parseMsgTarget(String(input.targetInput));
@@ -5442,13 +4423,19 @@ function registerMessageCommand(input) {
5442
4423
  process.exitCode = 1;
5443
4424
  }
5444
4425
  });
5445
- 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) => {
4426
+ 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 (optional when using --attach)").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)").option("--attach <path>", "Attach a local file path (repeatable)", collectOptionValue, []).action(async (...args) => {
5446
4427
  const [targetInput, text, options] = args;
4428
+ const hasAttach = (options.attach ?? []).length > 0;
4429
+ if (!text && !hasAttach) {
4430
+ console.error("Error: <text> is required when no --attach files are provided.");
4431
+ process.exitCode = 1;
4432
+ return;
4433
+ }
5447
4434
  try {
5448
4435
  const payload = await sendMessage({
5449
4436
  ctx: input.ctx,
5450
4437
  targetInput,
5451
- text,
4438
+ text: text ?? "",
5452
4439
  options
5453
4440
  });
5454
4441
  console.log(JSON.stringify(payload, null, 2));
@@ -6217,15 +5204,15 @@ function registerSearchCommand(input) {
6217
5204
  }
6218
5205
 
6219
5206
  // src/lib/update.ts
6220
- import { execSync as execSync3 } from "node:child_process";
5207
+ import { execSync as execSync2 } from "node:child_process";
6221
5208
  import { createHash } from "node:crypto";
6222
- import { chmod, copyFile as copyFile2, mkdir as mkdir5, readFile as readFile6, rename, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
5209
+ import { chmod, copyFile as copyFile2, mkdir as mkdir5, readFile as readFile7, rename, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
6223
5210
  import { tmpdir as tmpdir3 } from "node:os";
6224
- import { basename as basename2, join as join11 } from "node:path";
5211
+ import { basename as basename3, join as join13 } from "node:path";
6225
5212
  var REPO = "stablyai/agent-slack";
6226
5213
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
6227
5214
  function getCachePath() {
6228
- return join11(getAppDir(), "update-check.json");
5215
+ return join13(getAppDir(), "update-check.json");
6229
5216
  }
6230
5217
  function compareSemver(a, b) {
6231
5218
  const pa = a.replace(/^v/, "").split(".").map(Number);
@@ -6282,10 +5269,10 @@ async function checkForUpdate(force = false) {
6282
5269
  };
6283
5270
  }
6284
5271
  function detectInstallMethod() {
6285
- if (process.versions.bun && basename2(process.execPath) === "bun") {
5272
+ if (process.versions.bun && basename3(process.execPath) === "bun") {
6286
5273
  return "bun";
6287
5274
  }
6288
- const execName = basename2(process.execPath);
5275
+ const execName = basename3(process.execPath);
6289
5276
  if (["node", "nodejs", "node.exe"].includes(execName) || process.env.npm_execpath) {
6290
5277
  return "npm";
6291
5278
  }
@@ -6305,7 +5292,7 @@ function getUpdateCommand(method) {
6305
5292
  function performPackageManagerUpdate(method) {
6306
5293
  const cmd = getUpdateCommand(method);
6307
5294
  try {
6308
- execSync3(cmd, { stdio: ["inherit", "pipe", "inherit"] });
5295
+ execSync2(cmd, { stdio: ["inherit", "pipe", "inherit"] });
6309
5296
  return { success: true, message: `Updated agent-slack via: ${cmd}` };
6310
5297
  } catch (err) {
6311
5298
  const msg = err instanceof Error ? err.message : String(err);
@@ -6320,17 +5307,17 @@ function detectPlatformAsset() {
6320
5307
  return `agent-slack-${platform7}-${arch}${ext}`;
6321
5308
  }
6322
5309
  async function sha256(filePath) {
6323
- const data = await readFile6(filePath);
5310
+ const data = await readFile7(filePath);
6324
5311
  return createHash("sha256").update(data).digest("hex");
6325
5312
  }
6326
5313
  async function performUpdate(latest) {
6327
5314
  const asset = detectPlatformAsset();
6328
5315
  const tag = `v${latest}`;
6329
5316
  const baseUrl = `https://github.com/${REPO}/releases/download/${tag}`;
6330
- const tmp = join11(tmpdir3(), `agent-slack-update-${Date.now()}`);
5317
+ const tmp = join13(tmpdir3(), `agent-slack-update-${Date.now()}`);
6331
5318
  await mkdir5(tmp, { recursive: true });
6332
- const binTmp = join11(tmp, asset);
6333
- const sumsTmp = join11(tmp, "checksums-sha256.txt");
5319
+ const binTmp = join13(tmp, asset);
5320
+ const sumsTmp = join13(tmp, "checksums-sha256.txt");
6334
5321
  try {
6335
5322
  const [binResp, sumsResp] = await Promise.all([
6336
5323
  fetch(`${baseUrl}/${asset}`, { signal: AbortSignal.timeout(120000) }),
@@ -6445,36 +5432,47 @@ function registerUpdateCommand(input) {
6445
5432
  async function listUsers(client, options) {
6446
5433
  const limit = Math.min(Math.max(options?.limit ?? 200, 1), 1000);
6447
5434
  const includeBots = options?.includeBots ?? false;
6448
- const out = [];
6449
- let cursor = options?.cursor;
6450
- while (out.length < limit) {
6451
- const pageSize = Math.min(200, limit - out.length);
6452
- const resp = await client.api("users.list", {
6453
- limit: pageSize,
6454
- cursor
6455
- });
6456
- const members = asArray(resp.members).filter(isRecord5);
6457
- for (const m of members) {
6458
- const id = getString(m.id);
6459
- if (!id) {
6460
- continue;
6461
- }
6462
- if (!includeBots && m.is_bot) {
6463
- continue;
6464
- }
6465
- out.push(toCompactUser(m));
6466
- if (out.length >= limit) {
6467
- break;
5435
+ let next_cursor;
5436
+ const [out, dmMap] = await Promise.all([
5437
+ (async () => {
5438
+ const users = [];
5439
+ let cursor = options?.cursor;
5440
+ while (users.length < limit) {
5441
+ const pageSize = Math.min(200, limit - users.length);
5442
+ const resp = await client.api("users.list", { limit: pageSize, cursor });
5443
+ const members = asArray(resp.members).filter(isRecord5);
5444
+ for (const m of members) {
5445
+ const id = getString(m.id);
5446
+ if (!id) {
5447
+ continue;
5448
+ }
5449
+ if (!includeBots && m.is_bot) {
5450
+ continue;
5451
+ }
5452
+ users.push(toCompactUser(m));
5453
+ if (users.length >= limit) {
5454
+ break;
5455
+ }
5456
+ }
5457
+ const meta = isRecord5(resp.response_metadata) ? resp.response_metadata : null;
5458
+ const next = meta ? getString(meta.next_cursor) : undefined;
5459
+ if (!next) {
5460
+ break;
5461
+ }
5462
+ cursor = next;
5463
+ next_cursor = next;
6468
5464
  }
5465
+ return users;
5466
+ })(),
5467
+ fetchDmMap(client)
5468
+ ]);
5469
+ for (const u of out) {
5470
+ const dmId = dmMap.get(u.id);
5471
+ if (dmId) {
5472
+ u.dm_id = dmId;
6469
5473
  }
6470
- const meta = isRecord5(resp.response_metadata) ? resp.response_metadata : null;
6471
- const next = meta ? getString(meta.next_cursor) : undefined;
6472
- if (!next) {
6473
- return { users: out };
6474
- }
6475
- cursor = next;
6476
5474
  }
6477
- return { users: out, next_cursor: cursor };
5475
+ return { users: out, next_cursor };
6478
5476
  }
6479
5477
  async function getUser(client, input) {
6480
5478
  const trimmed = input.trim();
@@ -6542,6 +5540,32 @@ async function resolveUserId2(client, input) {
6542
5540
  }
6543
5541
  return null;
6544
5542
  }
5543
+ async function fetchDmMap(client) {
5544
+ const map = new Map;
5545
+ let cursor;
5546
+ for (;; ) {
5547
+ const resp = await client.api("conversations.list", {
5548
+ types: "im",
5549
+ limit: 200,
5550
+ cursor
5551
+ });
5552
+ const channels = asArray(resp.channels).filter(isRecord5);
5553
+ for (const ch of channels) {
5554
+ const id = getString(ch.id);
5555
+ const user = getString(ch.user);
5556
+ if (id && user) {
5557
+ map.set(user, id);
5558
+ }
5559
+ }
5560
+ const meta = isRecord5(resp.response_metadata) ? resp.response_metadata : null;
5561
+ const next = meta ? getString(meta.next_cursor) : undefined;
5562
+ if (!next) {
5563
+ break;
5564
+ }
5565
+ cursor = next;
5566
+ }
5567
+ return map;
5568
+ }
6545
5569
  function toCompactUser(u) {
6546
5570
  const profile = isRecord5(u.profile) ? u.profile : {};
6547
5571
  return {
@@ -6854,5 +5878,5 @@ if (subcommand && subcommand !== "update") {
6854
5878
  backgroundUpdateCheck();
6855
5879
  }
6856
5880
 
6857
- //# debugId=24F295C11738082464756E2164756E21
5881
+ //# debugId=DBCD03BFA3C520AC64756E2164756E21
6858
5882
  //# sourceMappingURL=index.js.map