ei-tui 1.3.4 → 1.4.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.
@@ -0,0 +1,167 @@
1
+ import type { CommandContext } from "./registry.js";
2
+ import { logger } from "../util/logger.js";
3
+ import {
4
+ generateVerifier,
5
+ generateChallenge,
6
+ buildAuthUrl,
7
+ exchangeCode,
8
+ } from "../../../src/core/tools/builtin/pkce.js";
9
+ import {
10
+ SLACK_CLIENT_ID,
11
+ SLACK_USER_SCOPES,
12
+ SLACK_TUI_REDIRECT_URI,
13
+ SLACK_TUI_PORT,
14
+ clearSlackTokenCache,
15
+ } from "../../../src/core/tools/builtin/slack-auth.js";
16
+
17
+ export async function runSlackAuth(ctx: CommandContext): Promise<void> {
18
+ logger.info("[slack-auth] runSlackAuth() called");
19
+ ctx.showNotification("Starting Slack auth — opening browser…", "info");
20
+
21
+ const verifier = generateVerifier();
22
+ const challenge = await generateChallenge(verifier);
23
+ logger.info("[slack-auth] PKCE verifier + challenge generated");
24
+
25
+ const authUrl = buildAuthUrl({
26
+ clientId: SLACK_CLIENT_ID,
27
+ redirectUri: SLACK_TUI_REDIRECT_URI,
28
+ scopes: [],
29
+ userScopes: SLACK_USER_SCOPES,
30
+ challenge,
31
+ authEndpoint: "https://slack.com/oauth/v2/authorize",
32
+ });
33
+ logger.info("[slack-auth] Auth URL built", { redirectUri: SLACK_TUI_REDIRECT_URI });
34
+
35
+ const codePromise = waitForAuthCode(ctx);
36
+
37
+ const openCmd = process.platform === "darwin"
38
+ ? "open"
39
+ : process.platform === "win32"
40
+ ? "cmd /c start"
41
+ : "xdg-open";
42
+
43
+ logger.info("[slack-auth] Spawning browser", { openCmd });
44
+ Bun.spawn([openCmd, authUrl], { stdio: ["ignore", "ignore", "ignore"] });
45
+ logger.info("[slack-auth] Browser spawned — awaiting OAuth callback…");
46
+
47
+ const code = await codePromise;
48
+ logger.info("[slack-auth] codePromise resolved", { gotCode: !!code });
49
+
50
+ if (!code) return;
51
+
52
+ ctx.showNotification("Exchanging auth code for tokens…", "info");
53
+
54
+ try {
55
+ logger.info("[slack-auth] Exchanging code for tokens");
56
+ const tokens = await exchangeCode({
57
+ code,
58
+ verifier,
59
+ redirectUri: SLACK_TUI_REDIRECT_URI,
60
+ clientId: SLACK_CLIENT_ID,
61
+ tokenEndpoint: "https://slack.com/api/oauth.v2.access",
62
+ tokenResponsePath: ["authed_user"],
63
+ });
64
+ logger.info("[slack-auth] Token exchange succeeded — storing tokens");
65
+
66
+ clearSlackTokenCache();
67
+
68
+ const team = tokens._raw.team as Record<string, string> | undefined;
69
+ const workspaceId = team?.id;
70
+ const workspaceName = team?.name;
71
+
72
+ const human = await ctx.ei.getHuman();
73
+ await ctx.ei.updateSettings({
74
+ slack: {
75
+ ...human.settings?.slack,
76
+ auth: {
77
+ type: "pkce",
78
+ token: tokens.access_token,
79
+ refresh_token: tokens.refresh_token,
80
+ workspace_id: workspaceId,
81
+ workspace_name: workspaceName,
82
+ },
83
+ },
84
+ });
85
+
86
+ logger.info("[slack-auth] Tokens stored — done!");
87
+ ctx.showNotification("✓ Slack connected successfully!", "info");
88
+ } catch (err) {
89
+ const msg = err instanceof Error ? err.message : String(err);
90
+ logger.error("[slack-auth] Token exchange failed", { msg });
91
+ ctx.showNotification(`Slack auth failed: ${msg}`, "error");
92
+ }
93
+ }
94
+
95
+ async function waitForAuthCode(ctx: CommandContext): Promise<string | null> {
96
+ return new Promise<string | null>((resolve) => {
97
+ const TIMEOUT_MS = 120_000;
98
+
99
+ let resolved = false;
100
+ let server: ReturnType<typeof Bun.serve> | null = null;
101
+
102
+ const finish = (code: string | null) => {
103
+ if (resolved) return;
104
+ resolved = true;
105
+ logger.info("[slack-auth] finish() called", { gotCode: !!code });
106
+ try { server?.stop(true); } catch { /* ignore */ }
107
+ clearTimeout(timer);
108
+ resolve(code);
109
+ };
110
+
111
+ const timer = setTimeout(() => {
112
+ logger.warn("[slack-auth] Timed out waiting for callback");
113
+ ctx.showNotification("Slack auth timed out (2 min)", "error");
114
+ finish(null);
115
+ }, TIMEOUT_MS);
116
+
117
+ try {
118
+ server = Bun.serve({
119
+ port: SLACK_TUI_PORT,
120
+ hostname: "127.0.0.1",
121
+ fetch(req) {
122
+ const url = new URL(req.url);
123
+ logger.info("[slack-auth] Incoming request", { method: req.method, path: url.pathname });
124
+
125
+ if (url.pathname !== "/") {
126
+ return new Response("Not found", { status: 404 });
127
+ }
128
+
129
+ const code = url.searchParams.get("code");
130
+ const error = url.searchParams.get("error");
131
+ logger.info("[slack-auth] Callback params", { hasCode: !!code, error });
132
+
133
+ if (error || !code) {
134
+ const msg = error ?? "no code in callback";
135
+ logger.error("[slack-auth] Auth denied or missing code", { msg });
136
+ ctx.showNotification(`Slack denied auth: ${msg}`, "error");
137
+ finish(null);
138
+ return new Response(
139
+ "<html><body><h2>Auth failed — return to your terminal.</h2></body></html>",
140
+ { headers: { "Content-Type": "text/html" } }
141
+ );
142
+ }
143
+
144
+ const resp = new Response(
145
+ "<html><head><meta charset=\"utf-8\"></head><body><h2>✓ Slack connected! You can close this tab.</h2></body></html>",
146
+ { headers: { "Content-Type": "text/html; charset=utf-8" } }
147
+ );
148
+ setTimeout(() => finish(code), 0);
149
+ return resp;
150
+ },
151
+ error(err) {
152
+ logger.error("[slack-auth] Bun.serve error", { msg: err.message });
153
+ ctx.showNotification(`Local auth server error: ${err.message}`, "error");
154
+ finish(null);
155
+ return new Response("Internal Server Error", { status: 500 });
156
+ },
157
+ });
158
+ logger.info("[slack-auth] Bun.serve started", { port: server.port });
159
+ } catch (err) {
160
+ const msg = err instanceof Error ? err.message : String(err);
161
+ logger.error("[slack-auth] Bun.serve failed to start", { msg });
162
+ ctx.showNotification(`Failed to start local auth server: ${msg}`, "error");
163
+ clearTimeout(timer);
164
+ resolve(null);
165
+ }
166
+ });
167
+ }
@@ -126,6 +126,7 @@ EXTENDED COMMANDS
126
126
  /auth
127
127
  Authenticate with an external service.
128
128
  /auth spotify Connect your Spotify account
129
+ /auth slack Connect your Slack workspace
129
130
 
130
131
  /queue
131
132
  Pause the queue and inspect or edit active items in $EDITOR.
@@ -1,6 +1,6 @@
1
1
  /** File-based logger for TUI debugging. Usage: tail -f $EI_DATA_PATH/tui.log */
2
2
 
3
- import { appendFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, renameSync } from "node:fs";
3
+ import { appendFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, copyFileSync, truncateSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { resolveDataPath } from "./resolve-data-path.js";
6
6
 
@@ -64,7 +64,8 @@ export function rotateLog(): void {
64
64
 
65
65
  if (existsSync(logPath)) {
66
66
  const ts = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
67
- renameSync(logPath, join(dataDir, `tui-${ts}.log`));
67
+ copyFileSync(logPath, join(dataDir, `tui-${ts}.log`));
68
+ truncateSync(logPath, 0);
68
69
  }
69
70
 
70
71
  const rolled = readdirSync(dataDir)
@@ -7,6 +7,7 @@ import type {
7
7
  } from "../../../src/core/types.js";
8
8
  import type { ClaudeCodeSettings } from "../../../src/integrations/claude-code/types.js";
9
9
  import type { CursorSettings } from "../../../src/integrations/cursor/types.js";
10
+ import type { SlackSettings } from "../../../src/integrations/slack/types.js";
10
11
  import { modelGuidToDisplay, displayToModelGuid } from "./yaml-shared.js";
11
12
  import { parseDuration, formatDuration } from "./duration.js";
12
13
 
@@ -46,6 +47,12 @@ interface EditableSettingsData {
46
47
  last_sync?: string | null;
47
48
  extraction_point?: string | null;
48
49
  };
50
+ slack?: {
51
+ integration?: boolean | null;
52
+ polling_interval_ms?: string | null;
53
+ last_sync?: string | null;
54
+ extraction_model?: string | null;
55
+ };
49
56
  backup?: {
50
57
  enabled?: boolean | null;
51
58
  max_backups?: number | null;
@@ -95,6 +102,12 @@ export function settingsToYAML(settings: HumanSettings | undefined, accounts: Pr
95
102
  last_sync: settings?.cursor?.last_sync ?? null,
96
103
  extraction_point: settings?.cursor?.extraction_point ?? null,
97
104
  },
105
+ slack: {
106
+ integration: settings?.slack?.integration ?? false,
107
+ polling_interval_ms: formatDuration(settings?.slack?.polling_interval_ms ?? 60000),
108
+ last_sync: settings?.slack?.last_sync ?? null,
109
+ extraction_model: guidToDisplay(settings?.slack?.extraction_model) ?? 'default',
110
+ },
98
111
  backup: {
99
112
  enabled: settings?.backup?.enabled ?? false,
100
113
  max_backups: settings?.backup?.max_backups ?? 24,
@@ -173,6 +186,17 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
173
186
  };
174
187
  }
175
188
 
189
+ let slack: SlackSettings | undefined;
190
+ if (data.slack) {
191
+ slack = {
192
+ ...original?.slack,
193
+ integration: nullToUndefined(data.slack.integration),
194
+ polling_interval_ms: parseMsDuration(data.slack.polling_interval_ms, 60000),
195
+ last_sync: original?.slack?.last_sync,
196
+ extraction_model: displayToGuid(data.slack.extraction_model),
197
+ };
198
+ }
199
+
176
200
  let backup: import('../../../src/core/types.js').BackupConfig | undefined;
177
201
  if (data.backup) {
178
202
  backup = {
@@ -197,6 +221,7 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
197
221
  opencode,
198
222
  claudeCode,
199
223
  cursor,
224
+ slack,
200
225
  backup,
201
226
  };
202
227
  }