slack-term 1.35.0 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -3
- package/SKILL.md +27 -9
- package/dist/cli.js +277 -141
- package/package.json +1 -1
- package/ts/ask.ts +14 -3
- package/ts/auth.ts +383 -54
- package/ts/chromeCookieWin.ts +230 -0
- package/ts/cli.ts +252 -42
- package/ts/leveldb.ts +154 -0
- package/ts/profiles.ts +73 -19
- package/ts/sentlog.ts +356 -0
- package/ts/slack-app.ts +287 -185
- package/ts/slack.ts +31 -5
package/package.json
CHANGED
package/ts/ask.ts
CHANGED
|
@@ -179,6 +179,12 @@ export function askBuildResolvedText(question: string, found: AskFound, who: str
|
|
|
179
179
|
return `${ASK_RESOLVED_PREFIX}*${question}*\n_${found.how}で回答済み${who ? ` (${who})` : ""}_\n\n${quoted}`;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/** Undo Slack's storage escaping. Only these three are ever escaped, and `&`
|
|
183
|
+
* goes LAST so an answer that literally contains `<` is not decoded twice. */
|
|
184
|
+
function askDecodeEntities(s: string): string {
|
|
185
|
+
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
186
|
+
}
|
|
187
|
+
|
|
182
188
|
export type AskParsed =
|
|
183
189
|
| { kind: "open"; question: string; reactable: string[]; overflow: string[]; threadOnly: boolean }
|
|
184
190
|
| { kind: "resolved"; question: string; answer: string }
|
|
@@ -222,10 +228,15 @@ export function askParseMessage(text: string): AskParsed {
|
|
|
222
228
|
// would re-poll a question that already has its answer.
|
|
223
229
|
if (text.startsWith(ASK_RESOLVED_PREFIX)) {
|
|
224
230
|
const head = lines[0]!.slice(ASK_RESOLVED_PREFIX.length);
|
|
225
|
-
const question = head.replace(/^\*/, "").replace(/\*$/, "");
|
|
231
|
+
const question = askDecodeEntities(head.replace(/^\*/, "").replace(/\*$/, ""));
|
|
232
|
+
// `> `, not only `> `: Slack HTML-escapes `&`, `<` and `>` in the text it
|
|
233
|
+
// hands back, so the quote we wrote as `> ` is STORED as `> `. Matching
|
|
234
|
+
// only the raw form made every collected question unreadable — re-running
|
|
235
|
+
// `--waitFor` on one exited 3 (measured 2026-09-27: 40 of 71 asks).
|
|
226
236
|
const answer = lines
|
|
227
|
-
.
|
|
228
|
-
.
|
|
237
|
+
.map((l) => (l.startsWith("> ") ? l.slice(2) : l.startsWith("> ") ? l.slice(5) : null))
|
|
238
|
+
.filter((l): l is string => l !== null)
|
|
239
|
+
.map(askDecodeEntities)
|
|
229
240
|
.join("\n");
|
|
230
241
|
// A ✅-stamped body with no quoted answer is not a settled question we can
|
|
231
242
|
// report; treat it as unknown rather than answer with an empty string.
|
package/ts/auth.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Interactive auth setup: slack auth login / slack login
|
|
2
2
|
import { createInterface, type Interface } from "node:readline/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { addProfile, listProfiles, setCookie, saveToEnvFile } from "./profiles.ts";
|
|
5
|
+
import { addProfile, listProfiles, setCookie, setUserCookie, saveToEnvFile, resolveToken, resolveCookie, resolveBotToken, type Profile } from "./profiles.ts";
|
|
6
6
|
import { authTest } from "./slack.ts";
|
|
7
|
-
import { extractSessions, discoverChromeCookies, discoverFirefoxCookies } from "./slack-app.ts";
|
|
7
|
+
import { extractSessions, extractChromeSessions, discoverChromeCookies, discoverChromeSessions, discoverFirefoxCookies, type ChromeSessionProfile } from "./slack-app.ts";
|
|
8
|
+
import { extractSlackXoxdWindows } from "./chromeCookieWin.ts";
|
|
8
9
|
|
|
9
10
|
const USER_SCOPES = [
|
|
10
11
|
"search:read",
|
|
@@ -53,9 +54,16 @@ async function ask(rl: Interface, q: string): Promise<string> {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
|
|
57
|
+
async function allowBrowserProfileRead(rl: Interface | undefined, yes: boolean): Promise<boolean> {
|
|
58
|
+
if (yes) return true;
|
|
59
|
+
if (!rl) return false;
|
|
60
|
+
const answer = await ask(rl, "Read local browser profiles and Slack session cookies? [y/N]: ");
|
|
61
|
+
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
async function saveToken(rl: Interface | null, token: string, nameOverride?: string, cookie?: string): Promise<string> {
|
|
57
65
|
console.error("Verifying token...");
|
|
58
|
-
const info = await authTest(token);
|
|
66
|
+
const info = await authTest(token, cookie);
|
|
59
67
|
const defaultName = slugify(info.team);
|
|
60
68
|
let name: string;
|
|
61
69
|
if (nameOverride) {
|
|
@@ -106,16 +114,53 @@ async function saveToken(rl: Interface | null, token: string, nameOverride?: str
|
|
|
106
114
|
}
|
|
107
115
|
|
|
108
116
|
/** Shared logic for importing sessions from the Slack desktop app. */
|
|
109
|
-
export async function importFromDesktop(
|
|
117
|
+
export async function importFromDesktop(
|
|
118
|
+
rl?: Interface, yes = false, browser: "auto" | "all" | "chrome" | "firefox" | "none" = "auto",
|
|
119
|
+
saveProfileOnly = false,
|
|
120
|
+
): Promise<void> {
|
|
110
121
|
console.error("Scanning Slack desktop app...");
|
|
111
122
|
const sessions = await extractSessions();
|
|
112
123
|
if (sessions.length === 0) {
|
|
113
124
|
console.error("No sessions found. Make sure Slack is installed and you have signed in at least once.");
|
|
114
125
|
process.exit(1);
|
|
115
126
|
}
|
|
127
|
+
if (process.platform === "linux" && browser !== "none" && sessions.some((s) => !s.cookie) && await allowBrowserProfileRead(rl, yes)) {
|
|
128
|
+
const found: { source: string; profileName: string; cookie: string }[] = [];
|
|
129
|
+
if (browser !== "chrome") {
|
|
130
|
+
for (const candidate of discoverFirefoxCookies()) {
|
|
131
|
+
found.push({ source: "Firefox", profileName: candidate.profileName, cookie: candidate.cookie });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (browser === "chrome" || browser === "all" || (browser === "auto" && found.length === 0)) {
|
|
135
|
+
try {
|
|
136
|
+
for (const candidate of discoverChromeCookies().candidates) {
|
|
137
|
+
found.push({ source: "Chrome", profileName: candidate.profileName, cookie: candidate.cookie });
|
|
138
|
+
}
|
|
139
|
+
} catch (e: unknown) {
|
|
140
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (found.length === 1) {
|
|
144
|
+
for (const session of sessions) session.cookie ??= found[0]!.cookie;
|
|
145
|
+
console.log(`Found Slack session cookie in ${found[0]!.source} profile: ${found[0]!.profileName}`);
|
|
146
|
+
} else if (found.length > 1) {
|
|
147
|
+
console.log("Multiple browser sessions found. Choose one later with slack auth chrome or slack auth firefox.");
|
|
148
|
+
}
|
|
149
|
+
if (sessions.some((s) => !s.cookie)) {
|
|
150
|
+
console.log("Desktop session needs an xoxd cookie on Linux. Run: slack auth chrome or slack auth firefox");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
116
153
|
|
|
117
|
-
|
|
118
|
-
|
|
154
|
+
await saveImportedSessions(sessions, rl, saveProfileOnly);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function saveImportedSessions(
|
|
158
|
+
sessions: import("./slack-app.ts").SlackAppSession[], rl?: Interface, saveProfileOnly = false,
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
// A session token without a cookie cannot pass auth.test yet. Save its
|
|
161
|
+
// LevelDB metadata as a profile so auth firefox can attach the cookie later.
|
|
162
|
+
// Single workspace + interactive + cookie: offer save-destination choice
|
|
163
|
+
if (sessions.length === 1 && rl && sessions[0]?.cookie && !saveProfileOnly) {
|
|
119
164
|
const s = sessions[0]!;
|
|
120
165
|
const teamLabel = s.teamName ?? s.teamId;
|
|
121
166
|
console.log(`Found workspace: ${teamLabel}${s.cookie ? " (+ xoxd cookie)" : ""}`);
|
|
@@ -220,9 +265,61 @@ async function loginNewApp(rl: Interface, mode: "user" | "bot"): Promise<void> {
|
|
|
220
265
|
* When run interactively, macOS will show a system dialog asking for the login password
|
|
221
266
|
* to grant access to the "Chrome Safe Storage" keychain item — click Allow.
|
|
222
267
|
*/
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Extract the Slack xoxd cookie from Chrome on Windows (app-bound "v20" scheme) for the
|
|
270
|
+
* selected workspace. Picks the Chrome profile that holds the workspace's session (else by
|
|
271
|
+
* --profile / prompt), then runs the elevated extraction (a UAC prompt appears).
|
|
272
|
+
*/
|
|
273
|
+
async function pickChromeCookieWindows(selected: { name: string; profile: Profile }, profileOpt?: string): Promise<string> {
|
|
274
|
+
const found = discoverChromeSessions();
|
|
275
|
+
if (found.length === 0) {
|
|
276
|
+
console.error("No Slack session found in Chrome. Sign in to Slack in Chrome (app.slack.com) and retry.");
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
// The workspace's xoxc token embeds its numeric team id — match the Chrome profile that has it.
|
|
280
|
+
const numericTeamId = selected.profile.token.split("-")[1];
|
|
281
|
+
let chosen = found.find((p) => p.sessions.some((s) => s.teamId === numericTeamId));
|
|
282
|
+
if (!chosen && profileOpt) {
|
|
283
|
+
chosen = found.find((p) => p.email === profileOpt || p.profileDir === profileOpt || p.profileDir === `Profile ${profileOpt}`);
|
|
284
|
+
if (!chosen) {
|
|
285
|
+
console.error(`Chrome profile "${profileOpt}" has no Slack session. Available: ${found.map((p) => p.email ?? p.profileDir).join(", ")}`);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (!chosen) {
|
|
290
|
+
if (found.length === 1) {
|
|
291
|
+
chosen = found[0]!;
|
|
292
|
+
} else if (process.stdin.isTTY) {
|
|
293
|
+
console.log("Multiple Chrome profiles have a Slack session:");
|
|
294
|
+
found.forEach((p, i) => console.log(` ${i + 1}) ${p.email ?? p.label} [${p.profileDir}]`));
|
|
295
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
296
|
+
const choice = (await rl.question(`Choice [1-${found.length}]: `)).trim();
|
|
297
|
+
rl.close();
|
|
298
|
+
const idx = parseInt(choice, 10) - 1;
|
|
299
|
+
if (isNaN(idx) || idx < 0 || idx >= found.length) { console.error("Invalid choice."); process.exit(1); }
|
|
300
|
+
chosen = found[idx]!;
|
|
301
|
+
} else {
|
|
302
|
+
console.error(`Multiple Chrome profiles have a Slack session — choose one with --profile:`);
|
|
303
|
+
found.forEach((p) => console.error(` slack auth cookie -w ${selected.name} --profile "${p.email ?? p.profileDir}"`));
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.log(`Using Chrome profile: ${chosen.email ?? chosen.label} [${chosen.profileDir}]`);
|
|
309
|
+
console.log("Chrome's cookie is app-bound encrypted; a Windows elevation (UAC) prompt will appear — approve it.");
|
|
310
|
+
try {
|
|
311
|
+
return await extractSlackXoxdWindows(chosen.profileDir);
|
|
312
|
+
} catch (e: unknown) {
|
|
313
|
+
console.error(`\nCould not extract the cookie automatically: ${e instanceof Error ? e.message : String(e)}`);
|
|
314
|
+
console.error("Fallback (manual): Chrome > DevTools (F12) > Application > Cookies > https://app.slack.com > copy the `d` value,");
|
|
315
|
+
console.error("then set it via an env file: SLACK_COOKIE=xoxd-... (in .slack-term/.env.local)");
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function cmdAuthChrome(opts: { workspace?: string; profile?: string; yes?: boolean } = {}): Promise<void> {
|
|
321
|
+
if (process.platform !== "darwin" && process.platform !== "linux" && process.platform !== "win32") {
|
|
322
|
+
console.error("Chrome cookie extraction is supported on macOS, Linux, and Windows.");
|
|
226
323
|
process.exit(1);
|
|
227
324
|
}
|
|
228
325
|
|
|
@@ -286,60 +383,172 @@ export async function cmdAuthChrome(opts: { workspace?: string } = {}): Promise<
|
|
|
286
383
|
process.exit(1);
|
|
287
384
|
}
|
|
288
385
|
|
|
289
|
-
|
|
290
|
-
|
|
386
|
+
let cookie: string;
|
|
387
|
+
if (process.platform === "win32") {
|
|
388
|
+
// Windows: app-bound (v20) extraction, elevated (UAC) — its own consent/flow.
|
|
389
|
+
cookie = await pickChromeCookieWindows(selected, opts.profile);
|
|
390
|
+
} else {
|
|
391
|
+
// Reading the browser's cookie store needs explicit consent (macOS keychain / Linux keyring).
|
|
392
|
+
if (!opts.yes && !process.stdin.isTTY) {
|
|
393
|
+
console.error("Reading browser profiles requires confirmation. Re-run interactively or pass --yes.");
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
const confirmRl = opts.yes ? undefined : createInterface({ input: process.stdin, output: process.stdout });
|
|
397
|
+
try {
|
|
398
|
+
if (!await allowBrowserProfileRead(confirmRl, opts.yes ?? false)) {
|
|
399
|
+
console.log("Browser profile scan cancelled.");
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
} finally {
|
|
403
|
+
confirmRl?.close();
|
|
404
|
+
}
|
|
405
|
+
console.log("Scanning Chrome profiles for Slack session...");
|
|
406
|
+
if (process.platform === "darwin") console.log("macOS may show a dialog asking for your login password — click Allow.");
|
|
407
|
+
|
|
408
|
+
let candidates: import("./slack-app.ts").ChromeCookieCandidate[];
|
|
409
|
+
let totalProfiles: number;
|
|
410
|
+
try {
|
|
411
|
+
({ candidates, totalProfiles } = discoverChromeCookies());
|
|
412
|
+
} catch (e: unknown) {
|
|
413
|
+
console.error(`Failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (candidates.length === 0) {
|
|
418
|
+
console.error(`No Slack session found in Chrome (scanned ${totalProfiles} profile${totalProfiles !== 1 ? "s" : ""}). Possible reasons:`);
|
|
419
|
+
console.error(" - You denied the keychain dialog (try running again and click Allow)");
|
|
420
|
+
console.error(" - Chrome is not installed or has no Slack session");
|
|
421
|
+
console.error(" - You're not logged in to Slack in Chrome");
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
291
424
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
425
|
+
// Always prompt when multiple Chrome profiles exist, so users can confirm the right one.
|
|
426
|
+
if (candidates.length === 1 && totalProfiles <= 1) {
|
|
427
|
+
cookie = candidates[0]!.cookie;
|
|
428
|
+
console.log(`Found session in Chrome profile: ${candidates[0]!.profileName}`);
|
|
429
|
+
} else {
|
|
430
|
+
const label = candidates.length === 1
|
|
431
|
+
? `Found 1 Slack session across ${totalProfiles} Chrome profiles:`
|
|
432
|
+
: `Found ${candidates.length} Slack sessions across ${totalProfiles} Chrome profiles:`;
|
|
433
|
+
console.log(label);
|
|
434
|
+
candidates.forEach((c, i) => console.log(` ${i + 1}) ${c.profileName} [${c.profileDir}]`));
|
|
435
|
+
console.log("");
|
|
436
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
437
|
+
const defaultChoice = candidates.length === 1 ? " [Enter=1]" : "";
|
|
438
|
+
const choice = (await rl.question(`Choice [1-${candidates.length}]${defaultChoice}: `)).trim();
|
|
439
|
+
rl.close();
|
|
440
|
+
const idx = choice === "" && candidates.length === 1 ? 0 : parseInt(choice, 10) - 1;
|
|
441
|
+
if (isNaN(idx) || idx < 0 || idx >= candidates.length) {
|
|
442
|
+
console.error("Invalid choice.");
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
cookie = candidates[idx]!.cookie;
|
|
446
|
+
}
|
|
299
447
|
}
|
|
300
448
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
449
|
+
setCookie(profileName, cookie);
|
|
450
|
+
console.log(`Saved xoxd cookie to workspace "${profileName}".`);
|
|
451
|
+
console.log(`RTM WebSocket mode is now available: slack tail @you`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Log in from Chrome: read xoxc workspace tokens from a Chrome profile's (plaintext)
|
|
456
|
+
* LocalStorage and create a workspace profile for each, grouped under the profile's
|
|
457
|
+
* account email as the identity. Works on all platforms for the token.
|
|
458
|
+
*
|
|
459
|
+
* The xoxd cookie (needed for RTM/drafts/search fallback) is captured on macOS via
|
|
460
|
+
* the keychain; on other platforms it must be attached separately (e.g. a live CDP
|
|
461
|
+
* read via rech, or a manual paste with `slack auth cookie`).
|
|
462
|
+
*/
|
|
463
|
+
export async function cmdAuthLoginChrome(opts: { profile?: string } = {}): Promise<void> {
|
|
464
|
+
console.error("Scanning Chrome profiles for Slack sessions...");
|
|
465
|
+
const found = discoverChromeSessions();
|
|
466
|
+
if (found.length === 0) {
|
|
467
|
+
console.error("No Slack session found in Chrome.");
|
|
468
|
+
console.error(" Open Slack in Chrome (https://app.slack.com) and sign in, then retry.");
|
|
306
469
|
process.exit(1);
|
|
307
470
|
}
|
|
308
471
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
472
|
+
// Pick a Chrome profile.
|
|
473
|
+
let chosen: ChromeSessionProfile;
|
|
474
|
+
if (opts.profile) {
|
|
475
|
+
const key = opts.profile;
|
|
476
|
+
const match = found.find(
|
|
477
|
+
(p) => p.email === key || p.profileDir === key || p.profileDir === `Profile ${key}`,
|
|
478
|
+
);
|
|
479
|
+
if (!match) {
|
|
480
|
+
console.error(`Chrome profile "${key}" has no Slack session. Available:`);
|
|
481
|
+
found.forEach((p) => console.error(` - ${p.email ?? p.label} [${p.profileDir}] (${p.sessions.length} workspace${p.sessions.length !== 1 ? "s" : ""})`));
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
chosen = match;
|
|
485
|
+
} else if (found.length === 1) {
|
|
486
|
+
chosen = found[0]!;
|
|
487
|
+
} else if (process.stdin.isTTY) {
|
|
488
|
+
console.log("Found Slack sessions in multiple Chrome profiles:");
|
|
489
|
+
found.forEach((p, i) => console.log(` ${i + 1}) ${p.email ?? p.label} [${p.profileDir}] (${p.sessions.length} workspace${p.sessions.length !== 1 ? "s" : ""})`));
|
|
321
490
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
322
|
-
const
|
|
323
|
-
const choice = (await rl.question(`Choice [1-${candidates.length}]${defaultChoice}: `)).trim();
|
|
491
|
+
const choice = (await rl.question(`Choice [1-${found.length}]: `)).trim();
|
|
324
492
|
rl.close();
|
|
325
|
-
const idx =
|
|
326
|
-
if (isNaN(idx) || idx < 0 || idx >=
|
|
493
|
+
const idx = parseInt(choice, 10) - 1;
|
|
494
|
+
if (isNaN(idx) || idx < 0 || idx >= found.length) {
|
|
327
495
|
console.error("Invalid choice.");
|
|
328
496
|
process.exit(1);
|
|
329
497
|
}
|
|
330
|
-
|
|
498
|
+
chosen = found[idx]!;
|
|
499
|
+
} else {
|
|
500
|
+
console.error(`Multiple Chrome profiles have a Slack session — choose one with --profile:`);
|
|
501
|
+
found.forEach((p) => console.error(` slack auth login chrome --profile "${p.email ?? p.profileDir}"`));
|
|
502
|
+
process.exit(1);
|
|
331
503
|
}
|
|
332
504
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
505
|
+
const identity = chosen.email ?? chosen.label ?? chosen.profileDir;
|
|
506
|
+
|
|
507
|
+
// Capture the shared xoxd cookie where possible (macOS keychain at rest).
|
|
508
|
+
let cookie: string | undefined;
|
|
509
|
+
if (process.platform === "darwin") {
|
|
510
|
+
try {
|
|
511
|
+
const { candidates } = discoverChromeCookies();
|
|
512
|
+
cookie = candidates.find((c) => c.profileDir === chosen.profileDir)?.cookie ?? candidates[0]?.cookie;
|
|
513
|
+
} catch {
|
|
514
|
+
// keychain denied or v20 — fall through to guidance below
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (cookie) setUserCookie(identity, cookie, `chrome:${chosen.profileDir}`);
|
|
518
|
+
|
|
519
|
+
for (const s of chosen.sessions) {
|
|
520
|
+
const teamLabel = s.teamName ?? s.teamId;
|
|
521
|
+
const name = slugify(teamLabel);
|
|
522
|
+
addProfile(name, {
|
|
523
|
+
token: s.token,
|
|
524
|
+
team: teamLabel,
|
|
525
|
+
teamId: s.teamId,
|
|
526
|
+
url: s.url ?? "",
|
|
527
|
+
user: "",
|
|
528
|
+
identity,
|
|
529
|
+
});
|
|
530
|
+
console.log(`Added workspace "${name}": ${teamLabel} (identity: ${identity})`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
console.log("");
|
|
534
|
+
if (cookie) {
|
|
535
|
+
console.log(`Captured xoxd cookie for ${identity} — shared across its ${chosen.sessions.length} workspace(s).`);
|
|
536
|
+
console.log(`RTM WebSocket mode is now available: slack tail @you`);
|
|
537
|
+
} else {
|
|
538
|
+
console.log("Note: no xoxd cookie captured (LocalStorage yields the token only; the cookie is");
|
|
539
|
+
console.log("encrypted at rest on this platform). Some features (RTM, drafts, search fallback) need it.");
|
|
540
|
+
console.log(" Attach it from your live browser — e.g. a CDP read via rech — or paste it:");
|
|
541
|
+
console.log(` slack auth cookie --workspace ${slugify(chosen.sessions[0]!.teamName ?? chosen.sessions[0]!.teamId)}`);
|
|
542
|
+
}
|
|
543
|
+
console.log("");
|
|
544
|
+
console.log("Select a workspace: slack auth use <name> (see: slack auth ls)");
|
|
336
545
|
}
|
|
337
546
|
|
|
338
547
|
/**
|
|
339
548
|
* Attach the xoxd session cookie from Firefox browser to an existing workspace profile.
|
|
340
549
|
* Firefox stores cookies in plaintext — no keychain access needed.
|
|
341
550
|
*/
|
|
342
|
-
export async function cmdAuthFirefox(opts: { workspace?: string } = {}): Promise<void> {
|
|
551
|
+
export async function cmdAuthFirefox(opts: { workspace?: string; yes?: boolean } = {}): Promise<void> {
|
|
343
552
|
const profiles = listProfiles();
|
|
344
553
|
if (profiles.length === 0) {
|
|
345
554
|
console.error("No workspaces configured. Run: slack auth login");
|
|
@@ -376,6 +585,19 @@ export async function cmdAuthFirefox(opts: { workspace?: string } = {}): Promise
|
|
|
376
585
|
}
|
|
377
586
|
}
|
|
378
587
|
|
|
588
|
+
if (!opts.yes && !process.stdin.isTTY) {
|
|
589
|
+
console.error("Reading browser profiles requires confirmation. Re-run interactively or pass --yes.");
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
const confirmRl = opts.yes ? undefined : createInterface({ input: process.stdin, output: process.stdout });
|
|
593
|
+
try {
|
|
594
|
+
if (!await allowBrowserProfileRead(confirmRl, opts.yes ?? false)) {
|
|
595
|
+
console.log("Browser profile scan cancelled.");
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
} finally {
|
|
599
|
+
confirmRl?.close();
|
|
600
|
+
}
|
|
379
601
|
console.log("Scanning Firefox profiles for Slack session...");
|
|
380
602
|
const candidates = discoverFirefoxCookies();
|
|
381
603
|
|
|
@@ -450,7 +672,44 @@ export async function cmdAuthApp(opts: { bot?: boolean } = {}): Promise<void> {
|
|
|
450
672
|
}
|
|
451
673
|
}
|
|
452
674
|
|
|
453
|
-
|
|
675
|
+
/** Print the currently resolved credentials as dotenv assignments. */
|
|
676
|
+
export function cmdAuthTokens(opts: { workspace?: string } = {}): void {
|
|
677
|
+
const format = (name: string, value: string): string => {
|
|
678
|
+
if (/[\r\n]/.test(value)) throw new Error(`Invalid newline in ${name}.`);
|
|
679
|
+
return `${name}=${/^[A-Za-z0-9._=-]+$/.test(value) ? value : JSON.stringify(value)}`;
|
|
680
|
+
};
|
|
681
|
+
const lines = [format("SLACK_TOKEN", resolveToken(opts.workspace))];
|
|
682
|
+
const cookie = resolveCookie(opts.workspace);
|
|
683
|
+
if (cookie) lines.push(format("SLACK_COOKIE", cookie));
|
|
684
|
+
if (!opts.workspace) {
|
|
685
|
+
const bot = resolveBotToken();
|
|
686
|
+
if (bot) lines.push(format("SLACK_BOT_TOKEN", bot));
|
|
687
|
+
}
|
|
688
|
+
console.log(lines.join("\n"));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** Export a selected profile for env-file based CLI use. */
|
|
692
|
+
export function cmdAuthSave(opts: { envfile: string; workspace?: string }): void {
|
|
693
|
+
const profiles = listProfiles();
|
|
694
|
+
const selected = opts.workspace
|
|
695
|
+
? profiles.find((entry) => entry.name === opts.workspace)
|
|
696
|
+
: profiles.find((entry) => entry.current) ?? (profiles.length === 1 ? profiles[0] : undefined);
|
|
697
|
+
if (!selected) {
|
|
698
|
+
throw new Error(opts.workspace
|
|
699
|
+
? `Workspace "${opts.workspace}" not found. Run: slack auth ls`
|
|
700
|
+
: "Select a workspace with slack auth use <name>, or pass --workspace <name>.");
|
|
701
|
+
}
|
|
702
|
+
if (!selected.profile.cookie) {
|
|
703
|
+
throw new Error(`Workspace "${selected.name}" has no session cookie. Run: slack auth chrome -w ${selected.name} or slack auth firefox -w ${selected.name}`);
|
|
704
|
+
}
|
|
705
|
+
const filePath = resolve(opts.envfile);
|
|
706
|
+
saveToEnvFile(filePath, { SLACK_TOKEN: selected.profile.token, SLACK_COOKIE: selected.profile.cookie });
|
|
707
|
+
console.log(`Saved token and cookie for workspace "${selected.name}" to ${filePath}`);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export async function cmdAuthLogin(opts: {
|
|
711
|
+
token?: string; name?: string; yes?: boolean; fromDesktop?: boolean; fromChrome?: boolean; fromFirefox?: boolean; fromAll?: boolean;
|
|
712
|
+
} = {}): Promise<void> {
|
|
454
713
|
// Show existing profiles if any
|
|
455
714
|
const existing = listProfiles();
|
|
456
715
|
if (existing.length > 0) {
|
|
@@ -462,6 +721,68 @@ export async function cmdAuthLogin(opts: { token?: string; name?: string } = {})
|
|
|
462
721
|
console.log("");
|
|
463
722
|
}
|
|
464
723
|
|
|
724
|
+
if ([opts.fromChrome, opts.fromFirefox, opts.fromAll].filter(Boolean).length > 1) {
|
|
725
|
+
throw new Error("Choose only one browser source: --from-chrome, --from-firefox, or --from-all.");
|
|
726
|
+
}
|
|
727
|
+
if (opts.fromChrome && !opts.fromDesktop) {
|
|
728
|
+
if (!opts.yes && !process.stdin.isTTY) {
|
|
729
|
+
throw new Error("Reading browser profiles requires confirmation. Re-run interactively or pass --yes.");
|
|
730
|
+
}
|
|
731
|
+
const rl = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : undefined;
|
|
732
|
+
try {
|
|
733
|
+
if (!await allowBrowserProfileRead(rl, opts.yes ?? false)) {
|
|
734
|
+
console.log("Browser profile scan cancelled.");
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const sessions = await extractChromeSessions();
|
|
738
|
+
if (sessions.length === 0) throw new Error("No Slack session token found in Chrome profiles.");
|
|
739
|
+
await saveImportedSessions(sessions, rl, true);
|
|
740
|
+
} finally {
|
|
741
|
+
rl?.close();
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (opts.fromAll && !opts.fromDesktop) {
|
|
746
|
+
if (!opts.yes && !process.stdin.isTTY) {
|
|
747
|
+
throw new Error("Reading browser profiles requires confirmation. Re-run interactively or pass --yes.");
|
|
748
|
+
}
|
|
749
|
+
const rl = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : undefined;
|
|
750
|
+
try {
|
|
751
|
+
try {
|
|
752
|
+
await importFromDesktop(rl, opts.yes ?? false, "all", true);
|
|
753
|
+
} catch (e: unknown) {
|
|
754
|
+
if (!(e instanceof Error) || !e.message.startsWith("Slack desktop app LevelDB not found")) throw e;
|
|
755
|
+
if (!await allowBrowserProfileRead(rl, opts.yes ?? false)) {
|
|
756
|
+
console.log("Browser profile scan cancelled.");
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const sessions = await extractChromeSessions();
|
|
760
|
+
if (sessions.length === 0) throw new Error("No Slack session token found in Chrome profiles.");
|
|
761
|
+
if (sessions.every((s) => !s.cookie)) {
|
|
762
|
+
const firefox = discoverFirefoxCookies();
|
|
763
|
+
if (firefox.length === 1) for (const session of sessions) session.cookie = firefox[0]!.cookie;
|
|
764
|
+
}
|
|
765
|
+
await saveImportedSessions(sessions, rl, true);
|
|
766
|
+
}
|
|
767
|
+
} finally {
|
|
768
|
+
rl?.close();
|
|
769
|
+
}
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (opts.fromDesktop || opts.fromChrome || opts.fromFirefox || opts.fromAll) {
|
|
773
|
+
const browser = opts.fromChrome ? "chrome" : opts.fromFirefox ? "firefox" : opts.fromAll ? "all" : "none";
|
|
774
|
+
if (browser !== "none" && !opts.yes && !process.stdin.isTTY) {
|
|
775
|
+
throw new Error("Reading browser profiles requires confirmation. Re-run interactively or pass --yes.");
|
|
776
|
+
}
|
|
777
|
+
const rl = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : undefined;
|
|
778
|
+
try {
|
|
779
|
+
await importFromDesktop(rl, opts.yes ?? false, browser, true);
|
|
780
|
+
} finally {
|
|
781
|
+
rl?.close();
|
|
782
|
+
}
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
465
786
|
// Non-interactive: token passed via --token flag or piped via stdin
|
|
466
787
|
if (opts.token) {
|
|
467
788
|
await saveToken(null, opts.token, opts.name);
|
|
@@ -485,33 +806,41 @@ export async function cmdAuthLogin(opts: { token?: string; name?: string } = {})
|
|
|
485
806
|
console.log("");
|
|
486
807
|
console.log(" 1) Slack desktop app - import session token");
|
|
487
808
|
console.log(" Reads the xoxc- token directly from the installed app.");
|
|
809
|
+
console.log(" Token: all platforms | xoxd cookie: macOS, or Chrome/Firefox on Linux");
|
|
810
|
+
console.log("");
|
|
811
|
+
console.log(" 2) Chrome browser - import workspaces");
|
|
812
|
+
console.log(" Reads xoxc- tokens from Chrome, grouped by account.");
|
|
488
813
|
console.log(" Token: all platforms | xoxd cookie: macOS only");
|
|
489
814
|
console.log("");
|
|
490
|
-
console.log("
|
|
815
|
+
console.log(" 3) Connect existing Slack app [recommended if you have one]");
|
|
491
816
|
console.log(" Paste a token from an app you already created.");
|
|
492
817
|
console.log("");
|
|
493
|
-
console.log("
|
|
818
|
+
console.log(" 4) Create new Slack app - user token (xoxp-)");
|
|
494
819
|
console.log(" Guided setup with manifest. Full access including search.");
|
|
495
820
|
console.log("");
|
|
496
|
-
console.log("
|
|
821
|
+
console.log(" 5) Create new Slack app - bot token (xoxb-)");
|
|
497
822
|
console.log(" Bot is invited to channels. Search and news unavailable.");
|
|
498
823
|
console.log("");
|
|
499
824
|
|
|
500
825
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
501
826
|
try {
|
|
502
|
-
const choice = await ask(rl, "Choice [1/2/3/4]: ");
|
|
827
|
+
const choice = await ask(rl, "Choice [1/2/3/4/5]: ");
|
|
503
828
|
console.log("");
|
|
504
829
|
|
|
505
830
|
if (choice === "1") {
|
|
506
|
-
await importFromDesktop(rl);
|
|
831
|
+
await importFromDesktop(rl, opts.yes ?? false);
|
|
507
832
|
} else if (choice === "2") {
|
|
508
|
-
|
|
833
|
+
rl.close();
|
|
834
|
+
await cmdAuthLoginChrome();
|
|
835
|
+
return;
|
|
509
836
|
} else if (choice === "3") {
|
|
510
|
-
await
|
|
837
|
+
await loginExisting(rl);
|
|
511
838
|
} else if (choice === "4") {
|
|
839
|
+
await loginNewApp(rl, "user");
|
|
840
|
+
} else if (choice === "5") {
|
|
512
841
|
await loginNewApp(rl, "bot");
|
|
513
842
|
} else {
|
|
514
|
-
console.error(`Invalid choice: "${choice}". Enter 1, 2, 3, or
|
|
843
|
+
console.error(`Invalid choice: "${choice}". Enter 1, 2, 3, 4, or 5.`);
|
|
515
844
|
process.exit(1);
|
|
516
845
|
}
|
|
517
846
|
} finally {
|