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/README.md +7 -2
- package/dist/index.js +1214 -2190
- package/dist/index.js.map +22 -15
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -48,17 +48,190 @@ function getUserAgent() {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// src/auth/brave.ts
|
|
51
|
-
import {
|
|
52
|
-
import { existsSync as
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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 =
|
|
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 (!
|
|
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 (!
|
|
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
|
|
254
|
-
import { platform as
|
|
255
|
-
var
|
|
256
|
-
function
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
384
|
-
import { join as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
684
|
-
var
|
|
685
|
-
var
|
|
686
|
-
var SLACK_SUPPORT_DIR_ELECTRON =
|
|
687
|
-
var SLACK_SUPPORT_DIR_APPSTORE =
|
|
688
|
-
var SLACK_SUPPORT_DIR_LINUX =
|
|
689
|
-
var SLACK_SUPPORT_DIR_LINUX_FLATPAK =
|
|
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 =
|
|
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 ${
|
|
829
|
+
throw new Error(`Slack Desktop extraction is not supported on ${PLATFORM2}.`);
|
|
694
830
|
}
|
|
695
831
|
for (const dir of candidates) {
|
|
696
|
-
const leveldbDir =
|
|
697
|
-
if (
|
|
698
|
-
const cookiesDbCandidates = [
|
|
699
|
-
const cookiesDb = cookiesDbCandidates.find((candidate) =>
|
|
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) =>
|
|
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 =
|
|
724
|
-
const dest =
|
|
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 = !
|
|
727
|
-
if (
|
|
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(
|
|
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 (!
|
|
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
|
|
955
|
+
await rm2(snap, { recursive: true, force: true });
|
|
820
956
|
} catch {}
|
|
821
957
|
}
|
|
822
958
|
}
|
|
823
959
|
function getSafeStoragePasswords2(prefix) {
|
|
824
|
-
if (
|
|
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 (
|
|
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,
|
|
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 (!
|
|
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 {
|
|
942
|
-
import {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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 (
|
|
960
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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 = [
|
|
1185
|
+
const roots = [join6(profilePath, "storage", "default")];
|
|
1221
1186
|
const candidates = [];
|
|
1222
1187
|
for (const root of roots) {
|
|
1223
|
-
if (!
|
|
1188
|
+
if (!existsSync5(root)) {
|
|
1224
1189
|
continue;
|
|
1225
1190
|
}
|
|
1226
|
-
candidates.push(
|
|
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 =
|
|
1234
|
-
if (!
|
|
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
|
|
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 =
|
|
1260
|
-
if (!
|
|
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
|
|
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
|
|
1322
|
-
var AGENT_SLACK_DIR =
|
|
1323
|
-
var CREDENTIALS_FILE =
|
|
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
|
|
1845
|
-
|
|
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
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
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
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
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
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
}),
|
|
2057
|
-
auth,
|
|
2058
|
-
workspace_url: normalizeUrl2(chosen.url)
|
|
2059
|
-
};
|
|
1929
|
+
if (result) {
|
|
1930
|
+
return result;
|
|
1931
|
+
}
|
|
2060
1932
|
}
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1966
|
+
return null;
|
|
2084
1967
|
}
|
|
1968
|
+
} else {
|
|
1969
|
+
return null;
|
|
2085
1970
|
}
|
|
2086
|
-
|
|
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:
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
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:
|
|
2097
|
-
xoxd_cookie:
|
|
2038
|
+
xoxc_token: team.token,
|
|
2039
|
+
xoxd_cookie: extracted.cookie_d
|
|
2098
2040
|
}
|
|
2099
|
-
});
|
|
2100
|
-
return
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
}),
|
|
2104
|
-
auth,
|
|
2105
|
-
workspace_url: normalizeUrl2(chosen.url)
|
|
2106
|
-
};
|
|
2041
|
+
})));
|
|
2042
|
+
return true;
|
|
2043
|
+
} catch {
|
|
2044
|
+
return false;
|
|
2107
2045
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
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
|
|
2390
|
-
import { existsSync as
|
|
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 =
|
|
2397
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
2413
|
+
return join9(xdg, "agent-slack");
|
|
2462
2414
|
}
|
|
2463
2415
|
const home = homedir5();
|
|
2464
2416
|
if (home) {
|
|
2465
|
-
return
|
|
2417
|
+
return join9(home, ".agent-slack");
|
|
2466
2418
|
}
|
|
2467
|
-
return
|
|
2419
|
+
return join9(tmpdir2(), "agent-slack");
|
|
2468
2420
|
}
|
|
2469
2421
|
|
|
2470
2422
|
// src/lib/tmp-paths.ts
|
|
2471
2423
|
function getDownloadsDir() {
|
|
2472
|
-
return resolve2(
|
|
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/
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
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
|
|
2653
|
-
if (
|
|
2654
|
-
return
|
|
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
|
|
2659
|
-
|
|
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
|
|
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
|
-
|
|
2799
|
-
|
|
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
|
-
|
|
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 (!
|
|
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() ||
|
|
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 =
|
|
2900
|
+
const pretext = getString3(a.pretext);
|
|
2888
2901
|
if (pretext) {
|
|
2889
2902
|
chunk.push(pretext);
|
|
2890
2903
|
}
|
|
2891
|
-
const title =
|
|
2892
|
-
const titleLink =
|
|
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 =
|
|
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 (!
|
|
2919
|
+
if (!isRecord8(f)) {
|
|
2907
2920
|
continue;
|
|
2908
2921
|
}
|
|
2909
|
-
const fieldTitle =
|
|
2910
|
-
const 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 =
|
|
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 =
|
|
2940
|
-
const authorLink =
|
|
2941
|
-
const fromUrl =
|
|
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 (!
|
|
2976
|
+
if (!isRecord8(mb)) {
|
|
2964
2977
|
continue;
|
|
2965
2978
|
}
|
|
2966
|
-
const message =
|
|
2979
|
+
const message = isRecord8(mb.message) ? mb.message : null;
|
|
2967
2980
|
if (!message) {
|
|
2968
2981
|
continue;
|
|
2969
2982
|
}
|
|
2970
|
-
const messageText =
|
|
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 (!
|
|
3029
|
+
if (!isRecord8(f)) {
|
|
3017
3030
|
continue;
|
|
3018
3031
|
}
|
|
3019
|
-
const name =
|
|
3020
|
-
const url =
|
|
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
|
|
3043
|
+
function isRecord8(value) {
|
|
3031
3044
|
return typeof value === "object" && value !== null;
|
|
3032
3045
|
}
|
|
3033
|
-
function
|
|
3046
|
+
function getString3(value) {
|
|
3034
3047
|
return typeof value === "string" ? value : "";
|
|
3035
3048
|
}
|
|
3036
3049
|
|
|
3037
|
-
// src/slack/message-
|
|
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-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
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
|
-
|
|
3463
|
-
|
|
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 (
|
|
3466
|
-
return
|
|
3526
|
+
if (!hasLists) {
|
|
3527
|
+
return null;
|
|
3467
3528
|
}
|
|
3468
|
-
|
|
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
|
|
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 =
|
|
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
|
|
3771
|
-
|
|
3943
|
+
await sendMessageToChannel({
|
|
3944
|
+
client,
|
|
3945
|
+
channelId: ref.channel_id,
|
|
3772
3946
|
text: input.text,
|
|
3773
|
-
|
|
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
|
|
3787
|
-
|
|
3788
|
-
|
|
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
|
|
3805
|
-
|
|
3983
|
+
await sendMessageToChannel({
|
|
3984
|
+
client,
|
|
3985
|
+
channelId,
|
|
3806
3986
|
text: input.text,
|
|
3807
|
-
|
|
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 (⌘B)"><span class="b">B</span></button>
|
|
4608
|
-
<button class="toolbar-btn" data-cmd="italic" data-tip="Italic (⌘I)"><span class="i">I</span></button>
|
|
4609
|
-
<button class="toolbar-btn" data-cmd="strikethrough" data-tip="Strikethrough (⌘⇧X)"><span class="s">S</span></button>
|
|
4610
|
-
<div class="toolbar-sep"></div>
|
|
4611
|
-
<button class="toolbar-btn" data-cmd="link" data-tip="Link (⌘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 (⌘⇧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 (⌘⇧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 (⌘⇧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 (⌘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 (⌘⇧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">⌘</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,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
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
|
|
4138
|
+
});
|
|
4139
|
+
return { ok: true };
|
|
4843
4140
|
}
|
|
4844
4141
|
|
|
4845
|
-
//
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
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
|
-
|
|
4178
|
+
return;
|
|
4878
4179
|
}
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
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
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
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
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
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
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
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
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5137
|
-
|
|
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
|
-
|
|
5188
|
-
|
|
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
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
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
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
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
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
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
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
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
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 &&
|
|
5272
|
+
if (process.versions.bun && basename3(process.execPath) === "bun") {
|
|
6286
5273
|
return "bun";
|
|
6287
5274
|
}
|
|
6288
|
-
const execName =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
5317
|
+
const tmp = join13(tmpdir3(), `agent-slack-update-${Date.now()}`);
|
|
6331
5318
|
await mkdir5(tmp, { recursive: true });
|
|
6332
|
-
const binTmp =
|
|
6333
|
-
const sumsTmp =
|
|
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
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
limit
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
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
|
|
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=
|
|
5881
|
+
//# debugId=DBCD03BFA3C520AC64756E2164756E21
|
|
6858
5882
|
//# sourceMappingURL=index.js.map
|