slack-term 1.34.0 → 1.36.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.34.0",
3
+ "version": "1.36.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/slack-term.git"
package/ts/agent.ts ADDED
@@ -0,0 +1,148 @@
1
+ import { spawn, type ChildProcess } from "node:child_process";
2
+ import { constants } from "node:os";
3
+ import type { Argv } from "yargs";
4
+ import { resolveBotToken } from "./profiles.ts";
5
+ import { renameAgentSession, resolveChannel, setAgentStatus, type AgentStatus, type AgentThread } from "./slack.ts";
6
+
7
+ const statuses: AgentStatus[] = ["processing", "active", "suspended", "closed"];
8
+ type ParseTarget = (target: string) => { ref: string; threadTs?: string };
9
+
10
+ function botToken(envName?: string): string {
11
+ if (envName !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(envName)) {
12
+ throw new Error("--bot-token-env must name an environment variable, not contain a token.");
13
+ }
14
+ const token = envName === undefined ? resolveBotToken() : process.env[envName];
15
+ if (!token?.startsWith("xoxb-")) {
16
+ throw new Error("Agent commands require a bot token (xoxb). Set SLACK_BOT_TOKEN or --bot-token-env NAME; user tokens are refused.");
17
+ }
18
+ return token;
19
+ }
20
+
21
+ function validateTitle(title?: string): void {
22
+ if (title !== undefined && (!title.trim() || [...title].length > 200)) {
23
+ throw new Error("--title must contain 1–200 characters.");
24
+ }
25
+ }
26
+
27
+ async function threadTarget(token: string, target: string, parse: ParseTarget): Promise<AgentThread> {
28
+ const { ref, threadTs } = parse(target);
29
+ if (!threadTs || !/^\d{10}\.\d{6}$/.test(threadTs)) {
30
+ throw new Error("Target must include a thread root: C00000001:1700000000.000100, #channel:ts, or a message permalink.");
31
+ }
32
+ return { channel_id: await resolveChannel(token, ref), thread_ts: threadTs };
33
+ }
34
+
35
+ function report(context: string, error: unknown): void {
36
+ console.error(`agent: ${context}: ${error instanceof Error ? error.message : "request failed"}`);
37
+ }
38
+
39
+ /** Own the command's lifetime, and serialize the last refresh before final cleanup. */
40
+ export async function runAgent(
41
+ token: string,
42
+ thread: AgentThread,
43
+ command: string[],
44
+ opts: { every: number; title?: string; close: boolean },
45
+ ): Promise<number> {
46
+ let child: ChildProcess | undefined;
47
+ let signal: NodeJS.Signals | undefined;
48
+ let killTimer: ReturnType<typeof setTimeout> | undefined;
49
+ let timer: ReturnType<typeof setInterval> | undefined;
50
+ let refreshing: Promise<void> | undefined;
51
+ let stopping = false;
52
+ let attempted = false;
53
+ let code = 1;
54
+
55
+ const stop = (received: NodeJS.Signals) => {
56
+ if (signal) return; // repeated interrupts must not bypass the final write
57
+ signal = received;
58
+ stopping = true;
59
+ if (child && child.exitCode === null && child.signalCode === null) {
60
+ child.kill(received);
61
+ killTimer = setTimeout(() => child?.kill("SIGKILL"), 5000);
62
+ }
63
+ };
64
+ const onInt = () => stop("SIGINT");
65
+ const onTerm = () => stop("SIGTERM");
66
+ process.on("SIGINT", onInt);
67
+ process.on("SIGTERM", onTerm);
68
+ try {
69
+ attempted = true; // even a lost response may have set processing remotely
70
+ await setAgentStatus(token, thread, "processing");
71
+ if (!stopping && opts.title !== undefined) await renameAgentSession(token, thread, opts.title);
72
+ if (!stopping) {
73
+ child = spawn(command[0]!, command.slice(1), { stdio: "inherit" });
74
+ const done = new Promise<number>((resolve) => {
75
+ child!.once("error", () => {
76
+ console.error("agent: could not start command");
77
+ resolve(127);
78
+ });
79
+ child!.once("exit", (exitCode, exitSignal) => {
80
+ resolve(exitCode ?? (exitSignal ? 128 + constants.signals[exitSignal] : 1));
81
+ });
82
+ });
83
+ timer = setInterval(() => {
84
+ if (stopping || refreshing) return;
85
+ refreshing = setAgentStatus(token, thread, "processing")
86
+ .then(() => {})
87
+ .catch((error: unknown) => report("heartbeat failed; retrying on next tick", error))
88
+ .finally(() => { refreshing = undefined; });
89
+ }, opts.every * 1000);
90
+ code = await done;
91
+ }
92
+ } catch (error) {
93
+ report("could not start agent session", error);
94
+ } finally {
95
+ stopping = true;
96
+ clearInterval(timer);
97
+ clearTimeout(killTimer);
98
+ // No late processing write may race with (and undo) the final active write.
99
+ await refreshing;
100
+ if (attempted) {
101
+ try {
102
+ await setAgentStatus(token, thread, opts.close ? "closed" : "active");
103
+ } catch (error) {
104
+ report("final status clear failed", error);
105
+ if (code === 0) code = 1;
106
+ }
107
+ }
108
+ process.removeListener("SIGINT", onInt);
109
+ process.removeListener("SIGTERM", onTerm);
110
+ }
111
+ return signal ? 128 + constants.signals[signal] : code;
112
+ }
113
+
114
+ export function agentCommands(y: Argv, parse: ParseTarget): Argv {
115
+ const common = (args: Argv) => args
116
+ .positional("target", { type: "string", demandOption: true, describe: "Thread root: raw channel ID:ts (avoids listing), #channel:ts, or permalink" })
117
+ .option("title", { type: "string", describe: "Rename the session (1–200 characters)" })
118
+ .option("bot-token-env", { type: "string", describe: "Environment variable holding an agent-enabled app's xoxb token (default SLACK_BOT_TOKEN); requires chat:write and channel membership" });
119
+ return y
120
+ .command("status <target> <status>", "Set a bot agent session's status", (args) => common(args)
121
+ .positional("status", { type: "string", choices: statuses, demandOption: true }), async (args) => {
122
+ validateTitle(args.title);
123
+ const token = botToken(args["bot-token-env"]);
124
+ const thread = await threadTarget(token, args.target, parse);
125
+ await setAgentStatus(token, thread, args.status as AgentStatus);
126
+ if (args.title !== undefined) await renameAgentSession(token, thread, args.title);
127
+ })
128
+ .command("run <target>", "Keep a thread working while a command runs: run <target> -- <command...>", (args) => common(args)
129
+ .parserConfiguration({ "populate--": true })
130
+ .option("every", { type: "number", default: 300, describe: "Heartbeat seconds (>0 and <3600); processing expires after one hour" })
131
+ .option("close", { type: "boolean", default: false, describe: "Finish with closed instead of active (also on failure or interrupt)" }), async (args) => {
132
+ validateTitle(args.title);
133
+ if (!Number.isFinite(args.every) || args.every <= 0 || args.every >= 3600) {
134
+ throw new Error("--every must be greater than 0 and less than 3600 seconds.");
135
+ }
136
+ // Preserve the exact argv, including numeric strings and the child's own --.
137
+ const separator = process.argv.indexOf("--");
138
+ const command = separator < 0 ? [] : process.argv.slice(separator + 1);
139
+ if (!command[0]) throw new Error("agent run requires -- <command...>");
140
+ const token = botToken(args["bot-token-env"]);
141
+ const thread = await threadTarget(token, args.target, parse);
142
+ process.exitCode = await runAgent(token, thread, command, {
143
+ every: args.every, close: args.close,
144
+ ...(args.title !== undefined ? { title: args.title } : {}),
145
+ });
146
+ })
147
+ .demandCommand(1, "Choose agent status or agent run.");
148
+ }
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 `&amp;`
183
+ * goes LAST so an answer that literally contains `&lt;` 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,10 @@
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, saveToEnvFile, resolveToken, resolveCookie, resolveBotToken } from "./profiles.ts";
6
6
  import { authTest } from "./slack.ts";
7
- import { extractSessions, discoverChromeCookies, discoverFirefoxCookies } from "./slack-app.ts";
7
+ import { extractSessions, extractChromeSessions, discoverChromeCookies, discoverFirefoxCookies } from "./slack-app.ts";
8
8
 
9
9
  const USER_SCOPES = [
10
10
  "search:read",
@@ -53,9 +53,16 @@ async function ask(rl: Interface, q: string): Promise<string> {
53
53
  }
54
54
 
55
55
 
56
+ async function allowBrowserProfileRead(rl: Interface | undefined, yes: boolean): Promise<boolean> {
57
+ if (yes) return true;
58
+ if (!rl) return false;
59
+ const answer = await ask(rl, "Read local browser profiles and Slack session cookies? [y/N]: ");
60
+ return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
61
+ }
62
+
56
63
  async function saveToken(rl: Interface | null, token: string, nameOverride?: string, cookie?: string): Promise<string> {
57
64
  console.error("Verifying token...");
58
- const info = await authTest(token);
65
+ const info = await authTest(token, cookie);
59
66
  const defaultName = slugify(info.team);
60
67
  let name: string;
61
68
  if (nameOverride) {
@@ -106,16 +113,53 @@ async function saveToken(rl: Interface | null, token: string, nameOverride?: str
106
113
  }
107
114
 
108
115
  /** Shared logic for importing sessions from the Slack desktop app. */
109
- export async function importFromDesktop(rl?: Interface): Promise<void> {
116
+ export async function importFromDesktop(
117
+ rl?: Interface, yes = false, browser: "auto" | "all" | "chrome" | "firefox" | "none" = "auto",
118
+ saveProfileOnly = false,
119
+ ): Promise<void> {
110
120
  console.error("Scanning Slack desktop app...");
111
121
  const sessions = await extractSessions();
112
122
  if (sessions.length === 0) {
113
123
  console.error("No sessions found. Make sure Slack is installed and you have signed in at least once.");
114
124
  process.exit(1);
115
125
  }
126
+ if (process.platform === "linux" && browser !== "none" && sessions.some((s) => !s.cookie) && await allowBrowserProfileRead(rl, yes)) {
127
+ const found: { source: string; profileName: string; cookie: string }[] = [];
128
+ if (browser !== "chrome") {
129
+ for (const candidate of discoverFirefoxCookies()) {
130
+ found.push({ source: "Firefox", profileName: candidate.profileName, cookie: candidate.cookie });
131
+ }
132
+ }
133
+ if (browser === "chrome" || browser === "all" || (browser === "auto" && found.length === 0)) {
134
+ try {
135
+ for (const candidate of discoverChromeCookies().candidates) {
136
+ found.push({ source: "Chrome", profileName: candidate.profileName, cookie: candidate.cookie });
137
+ }
138
+ } catch (e: unknown) {
139
+ console.error(e instanceof Error ? e.message : String(e));
140
+ }
141
+ }
142
+ if (found.length === 1) {
143
+ for (const session of sessions) session.cookie ??= found[0]!.cookie;
144
+ console.log(`Found Slack session cookie in ${found[0]!.source} profile: ${found[0]!.profileName}`);
145
+ } else if (found.length > 1) {
146
+ console.log("Multiple browser sessions found. Choose one later with slack auth chrome or slack auth firefox.");
147
+ }
148
+ if (sessions.some((s) => !s.cookie)) {
149
+ console.log("Desktop session needs an xoxd cookie on Linux. Run: slack auth chrome or slack auth firefox");
150
+ }
151
+ }
152
+
153
+ await saveImportedSessions(sessions, rl, saveProfileOnly);
154
+ }
116
155
 
117
- // Single workspace + interactive: offer save-destination choice
118
- if (sessions.length === 1 && rl) {
156
+ async function saveImportedSessions(
157
+ sessions: import("./slack-app.ts").SlackAppSession[], rl?: Interface, saveProfileOnly = false,
158
+ ): Promise<void> {
159
+ // A session token without a cookie cannot pass auth.test yet. Save its
160
+ // LevelDB metadata as a profile so auth firefox can attach the cookie later.
161
+ // Single workspace + interactive + cookie: offer save-destination choice
162
+ if (sessions.length === 1 && rl && sessions[0]?.cookie && !saveProfileOnly) {
119
163
  const s = sessions[0]!;
120
164
  const teamLabel = s.teamName ?? s.teamId;
121
165
  console.log(`Found workspace: ${teamLabel}${s.cookie ? " (+ xoxd cookie)" : ""}`);
@@ -220,9 +264,9 @@ async function loginNewApp(rl: Interface, mode: "user" | "bot"): Promise<void> {
220
264
  * When run interactively, macOS will show a system dialog asking for the login password
221
265
  * to grant access to the "Chrome Safe Storage" keychain item — click Allow.
222
266
  */
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.");
267
+ export async function cmdAuthChrome(opts: { workspace?: string; yes?: boolean } = {}): Promise<void> {
268
+ if (process.platform !== "darwin" && process.platform !== "linux") {
269
+ console.error("Chrome cookie extraction is supported on macOS and Linux.");
226
270
  process.exit(1);
227
271
  }
228
272
 
@@ -286,8 +330,21 @@ export async function cmdAuthChrome(opts: { workspace?: string } = {}): Promise<
286
330
  process.exit(1);
287
331
  }
288
332
 
333
+ if (!opts.yes && !process.stdin.isTTY) {
334
+ console.error("Reading browser profiles requires confirmation. Re-run interactively or pass --yes.");
335
+ process.exit(1);
336
+ }
337
+ const confirmRl = opts.yes ? undefined : createInterface({ input: process.stdin, output: process.stdout });
338
+ try {
339
+ if (!await allowBrowserProfileRead(confirmRl, opts.yes ?? false)) {
340
+ console.log("Browser profile scan cancelled.");
341
+ return;
342
+ }
343
+ } finally {
344
+ confirmRl?.close();
345
+ }
289
346
  console.log("Scanning Chrome profiles for Slack session...");
290
- console.log("macOS may show a dialog asking for your login password — click Allow.");
347
+ if (process.platform === "darwin") console.log("macOS may show a dialog asking for your login password — click Allow.");
291
348
 
292
349
  let candidates: import("./slack-app.ts").ChromeCookieCandidate[];
293
350
  let totalProfiles: number;
@@ -300,7 +357,7 @@ export async function cmdAuthChrome(opts: { workspace?: string } = {}): Promise<
300
357
 
301
358
  if (candidates.length === 0) {
302
359
  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)");
360
+ if (process.platform === "darwin") console.error(" - You denied the keychain dialog (try running again and click Allow)");
304
361
  console.error(" - Chrome is not installed or has no Slack session");
305
362
  console.error(" - You're not logged in to Slack in Chrome");
306
363
  process.exit(1);
@@ -339,7 +396,7 @@ export async function cmdAuthChrome(opts: { workspace?: string } = {}): Promise<
339
396
  * Attach the xoxd session cookie from Firefox browser to an existing workspace profile.
340
397
  * Firefox stores cookies in plaintext — no keychain access needed.
341
398
  */
342
- export async function cmdAuthFirefox(opts: { workspace?: string } = {}): Promise<void> {
399
+ export async function cmdAuthFirefox(opts: { workspace?: string; yes?: boolean } = {}): Promise<void> {
343
400
  const profiles = listProfiles();
344
401
  if (profiles.length === 0) {
345
402
  console.error("No workspaces configured. Run: slack auth login");
@@ -376,6 +433,19 @@ export async function cmdAuthFirefox(opts: { workspace?: string } = {}): Promise
376
433
  }
377
434
  }
378
435
 
436
+ if (!opts.yes && !process.stdin.isTTY) {
437
+ console.error("Reading browser profiles requires confirmation. Re-run interactively or pass --yes.");
438
+ process.exit(1);
439
+ }
440
+ const confirmRl = opts.yes ? undefined : createInterface({ input: process.stdin, output: process.stdout });
441
+ try {
442
+ if (!await allowBrowserProfileRead(confirmRl, opts.yes ?? false)) {
443
+ console.log("Browser profile scan cancelled.");
444
+ return;
445
+ }
446
+ } finally {
447
+ confirmRl?.close();
448
+ }
379
449
  console.log("Scanning Firefox profiles for Slack session...");
380
450
  const candidates = discoverFirefoxCookies();
381
451
 
@@ -450,7 +520,44 @@ export async function cmdAuthApp(opts: { bot?: boolean } = {}): Promise<void> {
450
520
  }
451
521
  }
452
522
 
453
- export async function cmdAuthLogin(opts: { token?: string; name?: string } = {}): Promise<void> {
523
+ /** Print the currently resolved credentials as dotenv assignments. */
524
+ export function cmdAuthTokens(opts: { workspace?: string } = {}): void {
525
+ const format = (name: string, value: string): string => {
526
+ if (/[\r\n]/.test(value)) throw new Error(`Invalid newline in ${name}.`);
527
+ return `${name}=${/^[A-Za-z0-9._=-]+$/.test(value) ? value : JSON.stringify(value)}`;
528
+ };
529
+ const lines = [format("SLACK_TOKEN", resolveToken(opts.workspace))];
530
+ const cookie = resolveCookie(opts.workspace);
531
+ if (cookie) lines.push(format("SLACK_COOKIE", cookie));
532
+ if (!opts.workspace) {
533
+ const bot = resolveBotToken();
534
+ if (bot) lines.push(format("SLACK_BOT_TOKEN", bot));
535
+ }
536
+ console.log(lines.join("\n"));
537
+ }
538
+
539
+ /** Export a selected profile for env-file based CLI use. */
540
+ export function cmdAuthSave(opts: { envfile: string; workspace?: string }): void {
541
+ const profiles = listProfiles();
542
+ const selected = opts.workspace
543
+ ? profiles.find((entry) => entry.name === opts.workspace)
544
+ : profiles.find((entry) => entry.current) ?? (profiles.length === 1 ? profiles[0] : undefined);
545
+ if (!selected) {
546
+ throw new Error(opts.workspace
547
+ ? `Workspace "${opts.workspace}" not found. Run: slack auth ls`
548
+ : "Select a workspace with slack auth use <name>, or pass --workspace <name>.");
549
+ }
550
+ if (!selected.profile.cookie) {
551
+ throw new Error(`Workspace "${selected.name}" has no session cookie. Run: slack auth chrome -w ${selected.name} or slack auth firefox -w ${selected.name}`);
552
+ }
553
+ const filePath = resolve(opts.envfile);
554
+ saveToEnvFile(filePath, { SLACK_TOKEN: selected.profile.token, SLACK_COOKIE: selected.profile.cookie });
555
+ console.log(`Saved token and cookie for workspace "${selected.name}" to ${filePath}`);
556
+ }
557
+
558
+ export async function cmdAuthLogin(opts: {
559
+ token?: string; name?: string; yes?: boolean; fromDesktop?: boolean; fromChrome?: boolean; fromFirefox?: boolean; fromAll?: boolean;
560
+ } = {}): Promise<void> {
454
561
  // Show existing profiles if any
455
562
  const existing = listProfiles();
456
563
  if (existing.length > 0) {
@@ -462,6 +569,68 @@ export async function cmdAuthLogin(opts: { token?: string; name?: string } = {})
462
569
  console.log("");
463
570
  }
464
571
 
572
+ if ([opts.fromChrome, opts.fromFirefox, opts.fromAll].filter(Boolean).length > 1) {
573
+ throw new Error("Choose only one browser source: --from-chrome, --from-firefox, or --from-all.");
574
+ }
575
+ if (opts.fromChrome && !opts.fromDesktop) {
576
+ if (!opts.yes && !process.stdin.isTTY) {
577
+ throw new Error("Reading browser profiles requires confirmation. Re-run interactively or pass --yes.");
578
+ }
579
+ const rl = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : undefined;
580
+ try {
581
+ if (!await allowBrowserProfileRead(rl, opts.yes ?? false)) {
582
+ console.log("Browser profile scan cancelled.");
583
+ return;
584
+ }
585
+ const sessions = await extractChromeSessions();
586
+ if (sessions.length === 0) throw new Error("No Slack session token found in Chrome profiles.");
587
+ await saveImportedSessions(sessions, rl, true);
588
+ } finally {
589
+ rl?.close();
590
+ }
591
+ return;
592
+ }
593
+ if (opts.fromAll && !opts.fromDesktop) {
594
+ if (!opts.yes && !process.stdin.isTTY) {
595
+ throw new Error("Reading browser profiles requires confirmation. Re-run interactively or pass --yes.");
596
+ }
597
+ const rl = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : undefined;
598
+ try {
599
+ try {
600
+ await importFromDesktop(rl, opts.yes ?? false, "all", true);
601
+ } catch (e: unknown) {
602
+ if (!(e instanceof Error) || !e.message.startsWith("Slack desktop app LevelDB not found")) throw e;
603
+ if (!await allowBrowserProfileRead(rl, opts.yes ?? false)) {
604
+ console.log("Browser profile scan cancelled.");
605
+ return;
606
+ }
607
+ const sessions = await extractChromeSessions();
608
+ if (sessions.length === 0) throw new Error("No Slack session token found in Chrome profiles.");
609
+ if (sessions.every((s) => !s.cookie)) {
610
+ const firefox = discoverFirefoxCookies();
611
+ if (firefox.length === 1) for (const session of sessions) session.cookie = firefox[0]!.cookie;
612
+ }
613
+ await saveImportedSessions(sessions, rl, true);
614
+ }
615
+ } finally {
616
+ rl?.close();
617
+ }
618
+ return;
619
+ }
620
+ if (opts.fromDesktop || opts.fromChrome || opts.fromFirefox || opts.fromAll) {
621
+ const browser = opts.fromChrome ? "chrome" : opts.fromFirefox ? "firefox" : opts.fromAll ? "all" : "none";
622
+ if (browser !== "none" && !opts.yes && !process.stdin.isTTY) {
623
+ throw new Error("Reading browser profiles requires confirmation. Re-run interactively or pass --yes.");
624
+ }
625
+ const rl = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : undefined;
626
+ try {
627
+ await importFromDesktop(rl, opts.yes ?? false, browser, true);
628
+ } finally {
629
+ rl?.close();
630
+ }
631
+ return;
632
+ }
633
+
465
634
  // Non-interactive: token passed via --token flag or piped via stdin
466
635
  if (opts.token) {
467
636
  await saveToken(null, opts.token, opts.name);
@@ -485,7 +654,7 @@ export async function cmdAuthLogin(opts: { token?: string; name?: string } = {})
485
654
  console.log("");
486
655
  console.log(" 1) Slack desktop app - import session token");
487
656
  console.log(" Reads the xoxc- token directly from the installed app.");
488
- console.log(" Token: all platforms | xoxd cookie: macOS only");
657
+ console.log(" Token: all platforms | xoxd cookie: macOS, or Chrome/Firefox on Linux");
489
658
  console.log("");
490
659
  console.log(" 2) Connect existing Slack app [recommended if you have one]");
491
660
  console.log(" Paste a token from an app you already created.");
@@ -503,7 +672,7 @@ export async function cmdAuthLogin(opts: { token?: string; name?: string } = {})
503
672
  console.log("");
504
673
 
505
674
  if (choice === "1") {
506
- await importFromDesktop(rl);
675
+ await importFromDesktop(rl, opts.yes ?? false);
507
676
  } else if (choice === "2") {
508
677
  await loginExisting(rl);
509
678
  } else if (choice === "3") {