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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-term",
3
- "version": "1.35.0",
3
+ "version": "1.37.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/slack-term.git"
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(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/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
+ // `&gt; `, not only `> `: Slack HTML-escapes `&`, `<` and `>` in the text it
233
+ // hands back, so the quote we wrote as `> ` is STORED as `&gt; `. 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
- .filter((l) => l.startsWith("> "))
228
- .map((l) => l.slice(2))
237
+ .map((l) => (l.startsWith("> ") ? l.slice(2) : l.startsWith("&gt; ") ? 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(rl?: Interface): Promise<void> {
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
- // Single workspace + interactive: offer save-destination choice
118
- if (sessions.length === 1 && rl) {
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
- export async function cmdAuthChrome(opts: { workspace?: string } = {}): Promise<void> {
224
- if (process.platform !== "darwin") {
225
- console.error("Chrome cookie extraction is only supported on macOS.");
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
- console.log("Scanning Chrome profiles for Slack session...");
290
- console.log("macOS may show a dialog asking for your login password — click Allow.");
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
- let candidates: import("./slack-app.ts").ChromeCookieCandidate[];
293
- let totalProfiles: number;
294
- try {
295
- ({ candidates, totalProfiles } = discoverChromeCookies());
296
- } catch (e: unknown) {
297
- console.error(`Failed: ${e instanceof Error ? e.message : String(e)}`);
298
- process.exit(1);
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
- if (candidates.length === 0) {
302
- console.error(`No Slack session found in Chrome (scanned ${totalProfiles} profile${totalProfiles !== 1 ? "s" : ""}). Possible reasons:`);
303
- console.error(" - You denied the keychain dialog (try running again and click Allow)");
304
- console.error(" - Chrome is not installed or has no Slack session");
305
- console.error(" - You're not logged in to Slack in Chrome");
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
- let cookie: string;
310
- // Always prompt when multiple Chrome profiles exist, so users can confirm the right one.
311
- if (candidates.length === 1 && totalProfiles <= 1) {
312
- cookie = candidates[0]!.cookie;
313
- console.log(`Found session in Chrome profile: ${candidates[0]!.profileName}`);
314
- } else {
315
- const label = candidates.length === 1
316
- ? `Found 1 Slack session across ${totalProfiles} Chrome profiles:`
317
- : `Found ${candidates.length} Slack sessions across ${totalProfiles} Chrome profiles:`;
318
- console.log(label);
319
- candidates.forEach((c, i) => console.log(` ${i + 1}) ${c.profileName} [${c.profileDir}]`));
320
- console.log("");
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 defaultChoice = candidates.length === 1 ? " [Enter=1]" : "";
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 = choice === "" && candidates.length === 1 ? 0 : parseInt(choice, 10) - 1;
326
- if (isNaN(idx) || idx < 0 || idx >= candidates.length) {
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
- cookie = candidates[idx]!.cookie;
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
- setCookie(profileName, cookie);
334
- console.log(`Saved xoxd cookie to workspace "${profileName}".`);
335
- console.log(`RTM WebSocket mode is now available: slack tail @you`);
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
- export async function cmdAuthLogin(opts: { token?: string; name?: string } = {}): Promise<void> {
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(" 2) Connect existing Slack app [recommended if you have one]");
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(" 3) Create new Slack app - user token (xoxp-)");
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(" 4) Create new Slack app - bot token (xoxb-)");
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
- await loginExisting(rl);
833
+ rl.close();
834
+ await cmdAuthLoginChrome();
835
+ return;
509
836
  } else if (choice === "3") {
510
- await loginNewApp(rl, "user");
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 4.`);
843
+ console.error(`Invalid choice: "${choice}". Enter 1, 2, 3, 4, or 5.`);
515
844
  process.exit(1);
516
845
  }
517
846
  } finally {