create-walle 0.4.2 → 0.4.3

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": "create-walle",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Set up Wall-E — your personal digital twin",
5
5
  "bin": {
6
6
  "create-walle": "bin/create-walle.js"
@@ -56,16 +56,44 @@ The first run auto-creates `.env`, `wall-e-config.json`, and `~/.walle/data/`. F
56
56
 
57
57
  All configuration lives in `.env` (auto-generated on first run). Edit directly or use the browser setup page.
58
58
 
59
+ ### API Authentication
60
+
61
+ Wall-E supports three ways to connect to Claude:
62
+
63
+ **Option A: Direct Anthropic API key**
64
+ ```env
65
+ ANTHROPIC_API_KEY=sk-ant-...
66
+ ```
67
+
68
+ **Option B: Portkey / API gateway**
69
+ ```env
70
+ ANTHROPIC_BASE_URL=https://your-gateway.example.com/v1
71
+ ANTHROPIC_AUTH_TOKEN=sk-ant-api03-unused
72
+ ANTHROPIC_CUSTOM_HEADERS_B64=<base64-encoded headers>
73
+ ```
74
+
75
+ **Option C: Corporate devbox (auto-detected)**
76
+ If you use `devbox ai -c claude`, Wall-E auto-reads your gateway credentials from `~/.devbox/secrets/claude/auth_headers` — no config needed.
77
+
78
+ ### Environment Variables
79
+
59
80
  | Variable | Required | Description |
60
81
  |---|---|---|
61
- | `ANTHROPIC_API_KEY` | Yes | Your Anthropic API key (set via browser setup) |
82
+ | `ANTHROPIC_API_KEY` | Yes* | Anthropic API key or Portkey key |
83
+ | `ANTHROPIC_BASE_URL` | No | API gateway URL (for Portkey, corporate proxies) |
84
+ | `ANTHROPIC_AUTH_TOKEN` | No | Auth token when using a gateway |
85
+ | `ANTHROPIC_CUSTOM_HEADERS_B64` | No | Base64-encoded custom headers (Portkey virtual keys, metadata) |
62
86
  | `WALLE_OWNER_NAME` | Auto | Your name (auto-detected from `git config`) |
87
+ | `CTM_PORT` | No | Dashboard port (default: `3456`) |
88
+ | `WALL_E_PORT` | No | Wall-E API port (default: `CTM_PORT + 1`) |
63
89
  | `WALL_E_DATA_DIR` | No | Data directory (default: `~/.walle/data`) |
64
90
  | `CTM_DATA_DIR` | No | CTM data directory (default: `~/.walle/data`) |
65
91
  | `SLACK_TOKEN` | No | Slack token (set via OAuth — click "Connect" in setup) |
66
92
  | `SLACK_OWNER_USER_ID` | No | Your Slack user ID |
67
93
  | `SLACK_OWNER_HANDLE` | No | Your Slack handle |
68
94
 
95
+ *Not required if using a gateway (`ANTHROPIC_BASE_URL`) or devbox auto-detection.
96
+
69
97
  ## Bundled Skills
70
98
 
71
99
  Wall-E ships with skills that run on a schedule to keep your brain up to date:
@@ -107,26 +107,6 @@ const server = http.createServer((req, res) => {
107
107
  }
108
108
  }
109
109
 
110
- // Slack OAuth callback (browser redirect — no auth token, must be before auth check)
111
- if (url.pathname === '/api/slack/callback' && req.method === 'GET') {
112
- try {
113
- const slackMcp = require('../wall-e/tools/slack-mcp');
114
- const code = url.searchParams.get('code');
115
- const state = url.searchParams.get('state');
116
- slackMcp.handleOAuthCallback(code, state).then(result => {
117
- res.writeHead(200, { 'Content-Type': 'text/html' });
118
- res.end(result.html);
119
- }).catch(err => {
120
- res.writeHead(500, { 'Content-Type': 'text/html' });
121
- res.end('<html><body>OAuth error: ' + err.message + '</body></html>');
122
- });
123
- } catch (e) {
124
- res.writeHead(500, { 'Content-Type': 'text/html' });
125
- res.end('<html><body>Slack module not available: ' + e.message + '</body></html>');
126
- }
127
- return;
128
- }
129
-
130
110
  // API routes
131
111
  if (url.pathname.startsWith('/api/')) {
132
112
  if (!isLocalhost(req)) {
@@ -1,4 +1,3 @@
1
- # Configuration
2
1
 
3
2
  Wall-E is configured through environment variables (`.env` file) and a JSON config file.
4
3
 
@@ -10,43 +9,74 @@ Copy `.env.example` to `.env` at the project root:
10
9
  cp .env.example .env
11
10
  ```
12
11
 
13
- ### Required
12
+ ### API Authentication
14
13
 
15
- | Variable | Description |
16
- |---|---|
17
- | `ANTHROPIC_API_KEY` | Your Anthropic API key. Required for Wall-E chat, think/reflect loops, and agent-mode skills. |
18
- | `WALLE_OWNER_NAME` | Your full name. Used in system prompts and memory attribution. |
14
+ Wall-E needs access to the Claude API. Three options:
19
15
 
20
- ### Data Storage
16
+ **Option A: Direct Anthropic API key**
21
17
 
22
- | Variable | Default | Description |
23
- |---|---|---|
24
- | `WALL_E_DATA_DIR` | `~/.walle/data` | Directory for Wall-E's brain database and backups |
25
- | `CTM_DATA_DIR` | `~/.walle/data` | Directory for CTM's database, images, and backups |
18
+ The simplest setup get a key from [console.anthropic.com](https://console.anthropic.com/settings/keys):
26
19
 
27
- ### Slack Integration {#slack}
20
+ ```env
21
+ ANTHROPIC_API_KEY=sk-ant-...
22
+ ```
28
23
 
29
- | Variable | Description |
30
- |---|---|
31
- | `SLACK_TOKEN` | Slack user or bot token (starts with `xoxb-` or `xoxp-`) |
32
- | `SLACK_BOT_TOKEN` | Slack bot token (for DM channel) |
33
- | `SLACK_OWNER_USER_ID` | Your Slack user ID (e.g., `U0XXXXXXXX`). Used to detect message direction. |
34
- | `SLACK_OWNER_HANDLE` | Your Slack handle (e.g., `your.name`). Used in search queries. |
24
+ **Option B: Portkey / API gateway**
35
25
 
36
- ### API Gateway
26
+ For teams using [Portkey](https://portkey.ai) or a corporate API gateway:
37
27
 
38
- | Variable | Description |
39
- |---|---|
40
- | `ANTHROPIC_BASE_URL` | Override API endpoint (e.g., for Portkey gateway) |
41
- | `ANTHROPIC_CUSTOM_HEADERS_B64` | Base64-encoded JSON headers for the gateway |
28
+ ```env
29
+ ANTHROPIC_BASE_URL=https://your-gateway.example.com/v1
30
+ ANTHROPIC_AUTH_TOKEN=your-portkey-api-key
31
+ ANTHROPIC_CUSTOM_HEADERS_B64=<base64-encoded custom headers>
32
+ ```
33
+
34
+ The custom headers carry your Portkey virtual key, provider config, and metadata. Encode them as base64:
35
+
36
+ ```bash
37
+ echo -n 'x-portkey-api-key: YOUR_KEY
38
+ x-portkey-provider: anthropic
39
+ x-portkey-virtual-key: YOUR_VIRTUAL_KEY' | base64
40
+ ```
41
+
42
+ **Option C: Corporate devbox (auto-detected)**
43
+
44
+ If your company manages Claude Code via devbox (`devbox ai -c claude`), Wall-E auto-reads the gateway credentials from `~/.devbox/secrets/claude/auth_headers` on startup. No configuration needed.
45
+
46
+ ### Core Settings
47
+
48
+ | Variable | Required | Default | Description |
49
+ |---|---|---|---|
50
+ | `ANTHROPIC_API_KEY` | Yes* | — | Anthropic API key or Portkey key |
51
+ | `ANTHROPIC_BASE_URL` | No | — | API gateway URL (Portkey, corporate proxies) |
52
+ | `ANTHROPIC_AUTH_TOKEN` | No | — | Auth token when using a gateway |
53
+ | `ANTHROPIC_CUSTOM_HEADERS_B64` | No | — | Base64-encoded custom headers (Portkey virtual keys) |
54
+ | `WALLE_OWNER_NAME` | Auto | from `git config` | Your full name |
55
+
56
+ *Not required if using a gateway or devbox auto-detection.
42
57
 
43
58
  ### Server
44
59
 
45
60
  | Variable | Default | Description |
46
61
  |---|---|---|
47
- | `CTM_PORT` | `3456` | CTM HTTP server port |
48
- | `CTM_HOST` | `127.0.0.1` | CTM bind address |
49
- | `WALL_E_PORT` | `3457` | Wall-E standalone HTTP port (when running separately) |
62
+ | `CTM_PORT` | `3456` | Dashboard port |
63
+ | `WALL_E_PORT` | `CTM_PORT + 1` | Wall-E API port |
64
+ | `CTM_HOST` | `127.0.0.1` | Bind address |
65
+
66
+ ### Data Storage
67
+
68
+ | Variable | Default | Description |
69
+ |---|---|---|
70
+ | `WALL_E_DATA_DIR` | `~/.walle/data` | Wall-E's brain database and backups |
71
+ | `CTM_DATA_DIR` | `~/.walle/data` | CTM's database, images, and backups |
72
+
73
+ ### Slack Integration {#slack}
74
+
75
+ | Variable | Description |
76
+ |---|---|
77
+ | `SLACK_TOKEN` | Slack user or bot token (set via OAuth in the setup page) |
78
+ | `SLACK_OWNER_USER_ID` | Your Slack user ID (e.g., `U0XXXXXXXX`) — for message direction detection |
79
+ | `SLACK_OWNER_HANDLE` | Your Slack handle (e.g., `your.name`) — for search queries |
50
80
 
51
81
  ## Wall-E Config File
52
82
 
@@ -14,43 +14,74 @@ Copy `.env.example` to `.env` at the project root:
14
14
  cp .env.example .env
15
15
  ```
16
16
 
17
- ### Required
17
+ ### API Authentication
18
18
 
19
- | Variable | Description |
20
- |---|---|
21
- | `ANTHROPIC_API_KEY` | Your Anthropic API key. Required for Wall-E chat, think/reflect loops, and agent-mode skills. |
22
- | `WALLE_OWNER_NAME` | Your full name. Used in system prompts and memory attribution. |
19
+ Wall-E needs access to the Claude API. Three options:
23
20
 
24
- ### Data Storage
21
+ **Option A: Direct Anthropic API key**
25
22
 
26
- | Variable | Default | Description |
27
- |---|---|---|
28
- | `WALL_E_DATA_DIR` | `~/.walle/data` | Directory for Wall-E's brain database and backups |
29
- | `CTM_DATA_DIR` | `~/.walle/data` | Directory for CTM's database, images, and backups |
23
+ The simplest setup get a key from [console.anthropic.com](https://console.anthropic.com/settings/keys):
30
24
 
31
- ### Slack Integration {#slack}
25
+ ```env
26
+ ANTHROPIC_API_KEY=sk-ant-...
27
+ ```
32
28
 
33
- | Variable | Description |
34
- |---|---|
35
- | `SLACK_TOKEN` | Slack user or bot token (starts with `xoxb-` or `xoxp-`) |
36
- | `SLACK_BOT_TOKEN` | Slack bot token (for DM channel) |
37
- | `SLACK_OWNER_USER_ID` | Your Slack user ID (e.g., `U0XXXXXXXX`). Used to detect message direction. |
38
- | `SLACK_OWNER_HANDLE` | Your Slack handle (e.g., `your.name`). Used in search queries. |
29
+ **Option B: Portkey / API gateway**
39
30
 
40
- ### API Gateway
31
+ For teams using [Portkey](https://portkey.ai) or a corporate API gateway:
41
32
 
42
- | Variable | Description |
43
- |---|---|
44
- | `ANTHROPIC_BASE_URL` | Override API endpoint (e.g., for Portkey gateway) |
45
- | `ANTHROPIC_CUSTOM_HEADERS_B64` | Base64-encoded JSON headers for the gateway |
33
+ ```env
34
+ ANTHROPIC_BASE_URL=https://your-gateway.example.com/v1
35
+ ANTHROPIC_AUTH_TOKEN=your-portkey-api-key
36
+ ANTHROPIC_CUSTOM_HEADERS_B64=<base64-encoded custom headers>
37
+ ```
38
+
39
+ The custom headers carry your Portkey virtual key, provider config, and metadata. Encode them as base64:
40
+
41
+ ```bash
42
+ echo -n 'x-portkey-api-key: YOUR_KEY
43
+ x-portkey-provider: anthropic
44
+ x-portkey-virtual-key: YOUR_VIRTUAL_KEY' | base64
45
+ ```
46
+
47
+ **Option C: Corporate devbox (auto-detected)**
48
+
49
+ If your company manages Claude Code via devbox (`devbox ai -c claude`), Wall-E auto-reads the gateway credentials from `~/.devbox/secrets/claude/auth_headers` on startup. No configuration needed.
50
+
51
+ ### Core Settings
52
+
53
+ | Variable | Required | Default | Description |
54
+ |---|---|---|---|
55
+ | `ANTHROPIC_API_KEY` | Yes* | — | Anthropic API key or Portkey key |
56
+ | `ANTHROPIC_BASE_URL` | No | — | API gateway URL (Portkey, corporate proxies) |
57
+ | `ANTHROPIC_AUTH_TOKEN` | No | — | Auth token when using a gateway |
58
+ | `ANTHROPIC_CUSTOM_HEADERS_B64` | No | — | Base64-encoded custom headers (Portkey virtual keys) |
59
+ | `WALLE_OWNER_NAME` | Auto | from `git config` | Your full name |
60
+
61
+ *Not required if using a gateway or devbox auto-detection.
46
62
 
47
63
  ### Server
48
64
 
49
65
  | Variable | Default | Description |
50
66
  |---|---|---|
51
- | `CTM_PORT` | `3456` | CTM HTTP server port |
52
- | `CTM_HOST` | `127.0.0.1` | CTM bind address |
53
- | `WALL_E_PORT` | `3457` | Wall-E standalone HTTP port (when running separately) |
67
+ | `CTM_PORT` | `3456` | Dashboard port |
68
+ | `WALL_E_PORT` | `CTM_PORT + 1` | Wall-E API port |
69
+ | `CTM_HOST` | `127.0.0.1` | Bind address |
70
+
71
+ ### Data Storage
72
+
73
+ | Variable | Default | Description |
74
+ |---|---|---|
75
+ | `WALL_E_DATA_DIR` | `~/.walle/data` | Wall-E's brain database and backups |
76
+ | `CTM_DATA_DIR` | `~/.walle/data` | CTM's database, images, and backups |
77
+
78
+ ### Slack Integration {#slack}
79
+
80
+ | Variable | Description |
81
+ |---|---|
82
+ | `SLACK_TOKEN` | Slack user or bot token (set via OAuth in the setup page) |
83
+ | `SLACK_OWNER_USER_ID` | Your Slack user ID (e.g., `U0XXXXXXXX`) — for message direction detection |
84
+ | `SLACK_OWNER_HANDLE` | Your Slack handle (e.g., `your.name`) — for search queries |
54
85
 
55
86
  ## Wall-E Config File
56
87
 
@@ -213,8 +213,12 @@ function handleWalleApi(req, res, url) {
213
213
  jsonResponse(res, { ok: true, already: true });
214
214
  return true;
215
215
  }
216
- // Start OAuth — opens browser, callback handled by CTM server route
217
- slackMcp.authenticate();
216
+ // Start OAuth — opens browser, temp server on port 3118 handles callback
217
+ slackMcp.authenticate().then(() => {
218
+ console.log('[wall-e] Slack OAuth completed');
219
+ }).catch(err => {
220
+ console.error('[wall-e] Slack OAuth failed:', err.message);
221
+ });
218
222
  jsonResponse(res, { ok: true, pending: true });
219
223
  } catch (e) {
220
224
  jsonResponse(res, { error: e.message }, 500);
@@ -1,19 +1,15 @@
1
1
  'use strict';
2
+ const http = require('http');
2
3
  const crypto = require('crypto');
3
4
  const fs = require('fs');
4
5
  const path = require('path');
5
6
 
6
7
  const SLACK_MCP_URL = 'https://mcp.slack.com/mcp';
7
8
  const CLIENT_ID = '1601185624273.8899143856786';
9
+ const CALLBACK_PORT = 3118; // Must match Slack's registered redirect_uri for this client_id
10
+ const CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`;
8
11
  const TOKEN_FILE = path.join(process.env.HOME, '.claude', 'wall-e-slack-token.json');
9
12
 
10
- // OAuth callback is handled by CTM server at /api/slack/callback
11
- // so it works on whatever port CTM is running on (no separate server needed).
12
- function getCallbackUrl() {
13
- const port = process.env.CTM_PORT || '3456';
14
- return `http://localhost:${port}/api/slack/callback`;
15
- }
16
-
17
13
  // ── Token persistence ──
18
14
 
19
15
  function loadToken() {
@@ -42,15 +38,15 @@ function generatePKCE() {
42
38
  return { verifier, challenge };
43
39
  }
44
40
 
45
- // Pending OAuth state (set during authenticate, consumed by handleOAuthCallback)
46
- let _pendingOAuth = null;
47
-
48
41
  /**
49
- * Start OAuth flow: opens browser, callback handled by CTM server.
50
- * Returns immediately — the token is saved when CTM receives the callback.
42
+ * Start OAuth flow: spins up a temporary callback server on port 3118
43
+ * (the registered redirect_uri for this Slack client_id), opens browser,
44
+ * and waits for Slack to redirect back with the auth code.
45
+ *
46
+ * The server runs inside the same Node process as CTM, so EDR/antivirus
47
+ * won't flag it as a new suspicious process.
51
48
  */
52
- function authenticate() {
53
- // Check if we already have a valid token
49
+ async function authenticate() {
54
50
  const existing = loadToken();
55
51
  if (existing && existing.access_token) {
56
52
  return existing.access_token;
@@ -58,89 +54,87 @@ function authenticate() {
58
54
 
59
55
  const { verifier, challenge } = generatePKCE();
60
56
  const state = crypto.randomBytes(16).toString('hex');
61
- const redirectUri = getCallbackUrl();
62
-
63
- _pendingOAuth = { verifier, state, redirectUri, createdAt: Date.now() };
64
-
65
- const scopes = 'search:read.public,search:read.private,search:read.mpim,search:read.im,search:read.files,search:read.users,chat:write,channels:history,groups:history,mpim:history,im:history,canvases:read,users:read,users:read.email';
66
- const authUrl = `https://slack.com/oauth/v2_user/authorize?client_id=${CLIENT_ID}&scope=${encodeURIComponent(scopes)}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}&code_challenge=${challenge}&code_challenge_method=S256`;
67
-
68
- console.log('[slack-mcp] Opening browser for Slack OAuth...');
69
- console.log('[slack-mcp] Callback URL:', redirectUri);
70
-
71
- const { execFile } = require('child_process');
72
- execFile('open', [authUrl], (err) => {
73
- if (err) console.error('[slack-mcp] Failed to open browser:', err.message);
74
- });
75
-
76
- return null; // Token not yet available — will be set by callback
77
- }
78
-
79
- /**
80
- * Handle the OAuth callback from Slack (called by CTM server route).
81
- * Returns { ok, html } or { error, html }.
82
- */
83
- async function handleOAuthCallback(code, returnedState) {
84
- if (!_pendingOAuth) {
85
- return { error: 'No pending OAuth flow. Click "Connect" again.', html: errorPage('No pending OAuth flow. Go back and click Connect again.') };
86
- }
87
-
88
- // Expire after 10 minutes
89
- if (Date.now() - _pendingOAuth.createdAt > 600000) {
90
- _pendingOAuth = null;
91
- return { error: 'OAuth flow expired.', html: errorPage('OAuth flow expired. Go back and click Connect again.') };
92
- }
93
-
94
- if (returnedState !== _pendingOAuth.state) {
95
- _pendingOAuth = null;
96
- return { error: 'State mismatch.', html: errorPage('OAuth state mismatch. Please try again.') };
97
- }
98
-
99
- if (!code) {
100
- _pendingOAuth = null;
101
- return { error: 'No code.', html: errorPage('No authorization code received from Slack.') };
102
- }
103
57
 
104
- try {
105
- const tokenResp = await fetch('https://slack.com/api/oauth.v2.user.access', {
106
- method: 'POST',
107
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
108
- body: new URLSearchParams({
109
- client_id: CLIENT_ID,
110
- code,
111
- code_verifier: _pendingOAuth.verifier,
112
- redirect_uri: _pendingOAuth.redirectUri,
113
- }),
58
+ return new Promise((resolve, reject) => {
59
+ const timeout = setTimeout(() => {
60
+ server.close();
61
+ reject(new Error('OAuth flow timed out (5min). Click Connect again.'));
62
+ }, 300000);
63
+
64
+ const server = http.createServer(async (req, res) => {
65
+ const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
66
+ if (url.pathname !== '/callback') { res.writeHead(404); res.end(); return; }
67
+
68
+ const code = url.searchParams.get('code');
69
+ const returnedState = url.searchParams.get('state');
70
+
71
+ if (returnedState !== state) {
72
+ res.writeHead(200, { 'Content-Type': 'text/html' });
73
+ res.end(errorPage('OAuth state mismatch. Please try again.'));
74
+ clearTimeout(timeout); server.close(); reject(new Error('State mismatch')); return;
75
+ }
76
+ if (!code) {
77
+ res.writeHead(200, { 'Content-Type': 'text/html' });
78
+ res.end(errorPage('No authorization code received.'));
79
+ clearTimeout(timeout); server.close(); reject(new Error('No code')); return;
80
+ }
81
+
82
+ try {
83
+ const tokenResp = await fetch('https://slack.com/api/oauth.v2.user.access', {
84
+ method: 'POST',
85
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
86
+ body: new URLSearchParams({ client_id: CLIENT_ID, code, code_verifier: verifier, redirect_uri: CALLBACK_URL }),
87
+ });
88
+ const tokenData = await tokenResp.json();
89
+ if (!tokenData.ok) throw new Error(tokenData.error);
90
+
91
+ const tokenInfo = {
92
+ access_token: tokenData.access_token || tokenData.authed_user?.access_token,
93
+ refresh_token: tokenData.refresh_token,
94
+ team_id: tokenData.team?.id || tokenData.team_id,
95
+ team_name: tokenData.team?.name,
96
+ user_id: tokenData.user_id || tokenData.authed_user?.id,
97
+ scope: tokenData.scope || tokenData.authed_user?.scope,
98
+ obtained_at: new Date().toISOString(),
99
+ };
100
+ saveToken(tokenInfo);
101
+
102
+ res.writeHead(200, { 'Content-Type': 'text/html' });
103
+ res.end(successPage());
104
+ clearTimeout(timeout); server.close();
105
+ console.log('[slack-mcp] OAuth completed — team:', tokenInfo.team_name);
106
+ resolve(tokenInfo.access_token);
107
+ } catch (err) {
108
+ res.writeHead(200, { 'Content-Type': 'text/html' });
109
+ res.end(errorPage('Token exchange failed: ' + err.message));
110
+ clearTimeout(timeout); server.close(); reject(err);
111
+ }
114
112
  });
115
- const tokenData = await tokenResp.json();
116
113
 
117
- _pendingOAuth = null;
114
+ server.listen(CALLBACK_PORT, () => {
115
+ const scopes = 'search:read.public,search:read.private,search:read.mpim,search:read.im,search:read.files,search:read.users,chat:write,channels:history,groups:history,mpim:history,im:history,canvases:read,users:read,users:read.email';
116
+ const authUrl = `https://slack.com/oauth/v2_user/authorize?client_id=${CLIENT_ID}&scope=${encodeURIComponent(scopes)}&redirect_uri=${encodeURIComponent(CALLBACK_URL)}&state=${state}&code_challenge=${challenge}&code_challenge_method=S256`;
117
+ console.log('[slack-mcp] OAuth callback listening on port', CALLBACK_PORT);
118
118
 
119
- if (!tokenData.ok) {
120
- return { error: tokenData.error, html: errorPage('Slack OAuth error: ' + tokenData.error) };
121
- }
119
+ const { execFile } = require('child_process');
120
+ execFile('open', [authUrl], (err) => {
121
+ if (err) console.error('[slack-mcp] Failed to open browser:', err.message);
122
+ });
123
+ });
122
124
 
123
- const tokenInfo = {
124
- access_token: tokenData.access_token || tokenData.authed_user?.access_token,
125
- refresh_token: tokenData.refresh_token,
126
- team_id: tokenData.team?.id || tokenData.team_id,
127
- team_name: tokenData.team?.name,
128
- user_id: tokenData.user_id || tokenData.authed_user?.id,
129
- scope: tokenData.scope || tokenData.authed_user?.scope,
130
- obtained_at: new Date().toISOString(),
131
- };
132
- saveToken(tokenInfo);
133
-
134
- console.log('[slack-mcp] OAuth completed — team:', tokenInfo.team_name);
135
- return { ok: true, html: successPage() };
136
- } catch (err) {
137
- _pendingOAuth = null;
138
- return { error: err.message, html: errorPage('Token exchange failed: ' + err.message) };
139
- }
125
+ server.on('error', (err) => {
126
+ clearTimeout(timeout);
127
+ if (err.code === 'EADDRINUSE') {
128
+ reject(new Error(`Port ${CALLBACK_PORT} is in use. Close any other OAuth flows and try again.`));
129
+ } else {
130
+ reject(new Error('OAuth callback server failed: ' + err.message));
131
+ }
132
+ });
133
+ });
140
134
  }
141
135
 
142
136
  function successPage() {
143
- return '<html><body style="font-family:system-ui;text-align:center;padding:60px;background:#0d1117;color:#e6edf3"><h2 style="color:#3fb950">Wall-E connected to Slack!</h2><p style="color:#8b949e">You can close this tab and return to the setup page.</p></body></html>';
137
+ return '<html><body style="font-family:system-ui;text-align:center;padding:60px;background:#0d1117;color:#e6edf3"><h2 style="color:#3fb950">Wall-E connected to Slack!</h2><p style="color:#8b949e">You can close this tab.</p></body></html>';
144
138
  }
145
139
 
146
140
  function errorPage(msg) {
@@ -281,4 +275,4 @@ if (require.main === module) {
281
275
  }
282
276
  }
283
277
 
284
- module.exports = { authenticate, handleOAuthCallback, callSlackMcp, listSlackTools, isAuthenticated, loadToken, clearToken };
278
+ module.exports = { authenticate, callSlackMcp, listSlackTools, isAuthenticated, loadToken, clearToken };