create-walle 0.3.0 → 0.3.2
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
|
@@ -243,13 +243,16 @@
|
|
|
243
243
|
document.getElementById('api-key').value = '';
|
|
244
244
|
document.getElementById('api-key').placeholder = '••••••••••••••• (from ' + (d.source || 'environment') + ')';
|
|
245
245
|
document.getElementById('api-dot').className = 'status-dot ok';
|
|
246
|
-
okMsg.textContent = '
|
|
246
|
+
okMsg.textContent = 'Detected: ' + (d.source || 'environment') + '!';
|
|
247
247
|
okMsg.style.display = 'inline';
|
|
248
248
|
const ownerVal = document.getElementById('owner-name').value.trim();
|
|
249
|
+
const saveBody = { owner_name: ownerVal };
|
|
250
|
+
if (d.gateway) saveBody.gateway = d.gateway;
|
|
251
|
+
else saveBody.api_key = d.key;
|
|
249
252
|
await fetch('/api/setup/save', {
|
|
250
253
|
method: 'POST',
|
|
251
254
|
headers: { 'Content-Type': 'application/json' },
|
|
252
|
-
body: JSON.stringify(
|
|
255
|
+
body: JSON.stringify(saveBody),
|
|
253
256
|
});
|
|
254
257
|
} else {
|
|
255
258
|
errMsg.textContent = d.hint || 'No API key found. Enter one manually.';
|
|
@@ -107,6 +107,26 @@ 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
|
+
|
|
110
130
|
// API routes
|
|
111
131
|
if (url.pathname.startsWith('/api/')) {
|
|
112
132
|
if (!isLocalhost(req)) {
|
|
@@ -208,15 +228,26 @@ function handleApi(req, res, url) {
|
|
|
208
228
|
if (url.pathname === '/api/setup/detect-key' && req.method === 'GET') {
|
|
209
229
|
let key = '';
|
|
210
230
|
let source = '';
|
|
231
|
+
let gateway = null; // For corporate/Portkey gateway setups
|
|
232
|
+
|
|
233
|
+
// 1. Check for corporate gateway setup (Portkey, cybertron, etc.)
|
|
234
|
+
if (process.env.ANTHROPIC_BASE_URL && process.env.ANTHROPIC_CUSTOM_HEADERS_B64) {
|
|
235
|
+
gateway = {
|
|
236
|
+
base_url: process.env.ANTHROPIC_BASE_URL,
|
|
237
|
+
auth_token: process.env.ANTHROPIC_AUTH_TOKEN || 'sk-ant-api03-unused',
|
|
238
|
+
custom_headers_b64: process.env.ANTHROPIC_CUSTOM_HEADERS_B64,
|
|
239
|
+
};
|
|
240
|
+
source = 'Claude Code gateway (' + process.env.ANTHROPIC_BASE_URL.replace(/https?:\/\//, '').split('/')[0] + ')';
|
|
241
|
+
}
|
|
211
242
|
|
|
212
|
-
//
|
|
213
|
-
if (process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY.startsWith('sk-ant-')) {
|
|
243
|
+
// 2. Check for direct API key in process.env
|
|
244
|
+
if (!gateway && process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY.startsWith('sk-ant-')) {
|
|
214
245
|
key = process.env.ANTHROPIC_API_KEY;
|
|
215
246
|
source = 'environment variable';
|
|
216
247
|
}
|
|
217
248
|
|
|
218
|
-
//
|
|
219
|
-
if (!key) {
|
|
249
|
+
// 3. Try shell profile for direct API key
|
|
250
|
+
if (!gateway && !key) {
|
|
220
251
|
try {
|
|
221
252
|
const { execFileSync } = require('child_process');
|
|
222
253
|
const shell = process.env.SHELL || '/bin/zsh';
|
|
@@ -225,8 +256,8 @@ function handleApi(req, res, url) {
|
|
|
225
256
|
} catch {}
|
|
226
257
|
}
|
|
227
258
|
|
|
228
|
-
//
|
|
229
|
-
if (!key && process.platform === 'darwin') {
|
|
259
|
+
// 4. Try Claude Code OAuth token from macOS Keychain
|
|
260
|
+
if (!gateway && !key && process.platform === 'darwin') {
|
|
230
261
|
try {
|
|
231
262
|
const { execFileSync } = require('child_process');
|
|
232
263
|
const credJson = execFileSync('security', ['find-generic-password', '-s', 'Claude Code-credentials', '-w'], { encoding: 'utf8', timeout: 3000 }).trim();
|
|
@@ -239,7 +270,6 @@ function handleApi(req, res, url) {
|
|
|
239
270
|
key = oauth.accessToken;
|
|
240
271
|
source = 'Claude Code (OAuth)';
|
|
241
272
|
} else {
|
|
242
|
-
// Token expired — tell user to refresh
|
|
243
273
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
244
274
|
res.end(JSON.stringify({
|
|
245
275
|
found: false,
|
|
@@ -254,10 +284,10 @@ function handleApi(req, res, url) {
|
|
|
254
284
|
}
|
|
255
285
|
|
|
256
286
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
257
|
-
if (
|
|
287
|
+
if (gateway) {
|
|
288
|
+
res.end(JSON.stringify({ found: true, gateway, source }));
|
|
289
|
+
} else if (key) {
|
|
258
290
|
res.end(JSON.stringify({ found: true, key, source }));
|
|
259
|
-
} else if (process.env.ANTHROPIC_BASE_URL) {
|
|
260
|
-
res.end(JSON.stringify({ found: false, hint: 'Your environment uses an API gateway (' + process.env.ANTHROPIC_BASE_URL + '). You may not need a separate API key — try going to the dashboard.' }));
|
|
261
291
|
} else {
|
|
262
292
|
res.end(JSON.stringify({ found: false, hint: 'No API key found. Checked: environment variables, shell profile, Claude Code keychain. You can get a key at console.anthropic.com' }));
|
|
263
293
|
}
|
|
@@ -281,6 +311,8 @@ function handleApi(req, res, url) {
|
|
|
281
311
|
const apiKey = typeof data.api_key === 'string'
|
|
282
312
|
? data.api_key.replace(/[\r\n\s]/g, '').slice(0, 200)
|
|
283
313
|
: '';
|
|
314
|
+
// Gateway config (corporate Portkey/cybertron setups)
|
|
315
|
+
const gw = data.gateway;
|
|
284
316
|
if (apiKey && !/^sk-ant-/.test(apiKey)) {
|
|
285
317
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
286
318
|
res.end(JSON.stringify({ error: 'API key must start with sk-ant-' }));
|
|
@@ -288,22 +320,40 @@ function handleApi(req, res, url) {
|
|
|
288
320
|
}
|
|
289
321
|
const envPath = path.resolve(__dirname, '..', '.env');
|
|
290
322
|
const lines = [];
|
|
291
|
-
// Read existing .env
|
|
323
|
+
// Read existing .env, strip lines we're about to replace
|
|
324
|
+
const stripPatterns = [/^#?\s*WALLE_OWNER_NAME=/, /^#?\s*ANTHROPIC_API_KEY=/, /^#?\s*ANTHROPIC_BASE_URL=/, /^#?\s*ANTHROPIC_AUTH_TOKEN=/, /^#?\s*ANTHROPIC_CUSTOM_HEADERS_B64=/];
|
|
292
325
|
try {
|
|
293
326
|
const existing = fs.readFileSync(envPath, 'utf8');
|
|
294
327
|
for (const line of existing.split('\n')) {
|
|
295
|
-
|
|
296
|
-
if (
|
|
297
|
-
|
|
328
|
+
const shouldStrip = (apiKey || gw) && stripPatterns.some(p => p.test(line));
|
|
329
|
+
if (!shouldStrip || (!apiKey && !gw)) lines.push(line);
|
|
330
|
+
else if (!ownerName || !line.match(/WALLE_OWNER_NAME/)) {
|
|
331
|
+
// Only strip if we have a replacement
|
|
332
|
+
if (stripPatterns.some(p => p.test(line))) continue;
|
|
333
|
+
lines.push(line);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Also strip owner name line if we have a new one
|
|
337
|
+
if (ownerName) {
|
|
338
|
+
const idx = lines.findIndex(l => /^#?\s*WALLE_OWNER_NAME=/.test(l));
|
|
339
|
+
if (idx >= 0) lines.splice(idx, 1);
|
|
298
340
|
}
|
|
299
341
|
} catch { lines.push('# Wall-E configuration'); lines.push(''); }
|
|
300
|
-
// Add values
|
|
342
|
+
// Add values
|
|
301
343
|
if (ownerName) {
|
|
302
344
|
const insertIdx = lines.findIndex(l => !l.startsWith('#') && l.trim() !== '') || lines.length;
|
|
303
345
|
lines.splice(insertIdx, 0, `WALLE_OWNER_NAME=${ownerName}`);
|
|
304
346
|
process.env.WALLE_OWNER_NAME = ownerName;
|
|
305
347
|
}
|
|
306
|
-
if (
|
|
348
|
+
if (gw) {
|
|
349
|
+
// Gateway setup: save all three env vars
|
|
350
|
+
lines.push(`ANTHROPIC_BASE_URL=${gw.base_url}`);
|
|
351
|
+
lines.push(`ANTHROPIC_AUTH_TOKEN=${gw.auth_token}`);
|
|
352
|
+
lines.push(`ANTHROPIC_CUSTOM_HEADERS_B64=${gw.custom_headers_b64}`);
|
|
353
|
+
process.env.ANTHROPIC_BASE_URL = gw.base_url;
|
|
354
|
+
process.env.ANTHROPIC_AUTH_TOKEN = gw.auth_token;
|
|
355
|
+
process.env.ANTHROPIC_CUSTOM_HEADERS_B64 = gw.custom_headers_b64;
|
|
356
|
+
} else if (apiKey) {
|
|
307
357
|
lines.push(`ANTHROPIC_API_KEY=${apiKey}`);
|
|
308
358
|
process.env.ANTHROPIC_API_KEY = apiKey;
|
|
309
359
|
}
|
|
@@ -213,13 +213,8 @@ function handleWalleApi(req, res, url) {
|
|
|
213
213
|
jsonResponse(res, { ok: true, already: true });
|
|
214
214
|
return true;
|
|
215
215
|
}
|
|
216
|
-
// Start OAuth —
|
|
217
|
-
slackMcp.authenticate()
|
|
218
|
-
console.log('[wall-e] Slack OAuth completed');
|
|
219
|
-
}).catch(err => {
|
|
220
|
-
console.error('[wall-e] Slack OAuth failed:', err.message);
|
|
221
|
-
});
|
|
222
|
-
// Tell client the flow started (browser will open)
|
|
216
|
+
// Start OAuth — opens browser, callback handled by CTM server route
|
|
217
|
+
slackMcp.authenticate();
|
|
223
218
|
jsonResponse(res, { ok: true, pending: true });
|
|
224
219
|
} catch (e) {
|
|
225
220
|
jsonResponse(res, { error: e.message }, 500);
|
|
@@ -6,9 +6,15 @@ const path = require('path');
|
|
|
6
6
|
|
|
7
7
|
const SLACK_MCP_URL = 'https://mcp.slack.com/mcp';
|
|
8
8
|
const CLIENT_ID = '1601185624273.8899143856786';
|
|
9
|
-
const CALLBACK_PORT = 3118; // Must match the registered redirect_uri for this client ID
|
|
10
9
|
const TOKEN_FILE = path.join(process.env.HOME, '.claude', 'wall-e-slack-token.json');
|
|
11
10
|
|
|
11
|
+
// OAuth callback is handled by CTM server at /api/slack/callback
|
|
12
|
+
// so it works on whatever port CTM is running on (no separate server needed).
|
|
13
|
+
function getCallbackUrl() {
|
|
14
|
+
const port = process.env.CTM_PORT || '3456';
|
|
15
|
+
return `http://localhost:${port}/api/slack/callback`;
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
// ── Token persistence ──
|
|
13
19
|
|
|
14
20
|
function loadToken() {
|
|
@@ -37,120 +43,109 @@ function generatePKCE() {
|
|
|
37
43
|
return { verifier, challenge };
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
// Pending OAuth state (set during authenticate, consumed by handleOAuthCallback)
|
|
47
|
+
let _pendingOAuth = null;
|
|
48
|
+
|
|
40
49
|
/**
|
|
41
|
-
* Start OAuth flow: opens browser,
|
|
42
|
-
* Returns the
|
|
50
|
+
* Start OAuth flow: opens browser, callback handled by CTM server.
|
|
51
|
+
* Returns immediately — the token is saved when CTM receives the callback.
|
|
43
52
|
*/
|
|
44
|
-
|
|
53
|
+
function authenticate() {
|
|
45
54
|
// Check if we already have a valid token
|
|
46
55
|
const existing = loadToken();
|
|
47
56
|
if (existing && existing.access_token) {
|
|
48
|
-
// TODO: check expiry if available
|
|
49
57
|
return existing.access_token;
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
const { verifier, challenge } = generatePKCE();
|
|
53
61
|
const state = crypto.randomBytes(16).toString('hex');
|
|
62
|
+
const redirectUri = getCallbackUrl();
|
|
54
63
|
|
|
55
|
-
|
|
56
|
-
const timeout = setTimeout(() => {
|
|
57
|
-
server.close();
|
|
58
|
-
reject(new Error('OAuth flow timed out (120s). Please try again.'));
|
|
59
|
-
}, 120000);
|
|
60
|
-
|
|
61
|
-
const server = http.createServer(async (req, res) => {
|
|
62
|
-
const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
|
|
63
|
-
if (url.pathname !== '/callback') {
|
|
64
|
-
res.writeHead(404);
|
|
65
|
-
res.end('Not found');
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const code = url.searchParams.get('code');
|
|
70
|
-
const returnedState = url.searchParams.get('state');
|
|
71
|
-
|
|
72
|
-
if (returnedState !== state) {
|
|
73
|
-
res.writeHead(400);
|
|
74
|
-
res.end('State mismatch. Please try again.');
|
|
75
|
-
clearTimeout(timeout);
|
|
76
|
-
server.close();
|
|
77
|
-
reject(new Error('OAuth state mismatch'));
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!code) {
|
|
82
|
-
res.writeHead(400);
|
|
83
|
-
res.end('No authorization code received.');
|
|
84
|
-
clearTimeout(timeout);
|
|
85
|
-
server.close();
|
|
86
|
-
reject(new Error('No authorization code'));
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
// Exchange code for token (MCP uses v2.user.access endpoint)
|
|
92
|
-
const tokenResp = await fetch('https://slack.com/api/oauth.v2.user.access', {
|
|
93
|
-
method: 'POST',
|
|
94
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
95
|
-
body: new URLSearchParams({
|
|
96
|
-
client_id: CLIENT_ID,
|
|
97
|
-
code,
|
|
98
|
-
code_verifier: verifier,
|
|
99
|
-
redirect_uri: `http://localhost:${CALLBACK_PORT}/callback`,
|
|
100
|
-
}),
|
|
101
|
-
});
|
|
102
|
-
const tokenData = await tokenResp.json();
|
|
103
|
-
|
|
104
|
-
if (!tokenData.ok) {
|
|
105
|
-
throw new Error(`Slack OAuth error: ${tokenData.error}`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Save token (v2.user.access returns tokens at top level, not nested under authed_user)
|
|
109
|
-
const tokenInfo = {
|
|
110
|
-
access_token: tokenData.access_token || tokenData.authed_user?.access_token,
|
|
111
|
-
refresh_token: tokenData.refresh_token,
|
|
112
|
-
team_id: tokenData.team?.id || tokenData.team_id,
|
|
113
|
-
team_name: tokenData.team?.name,
|
|
114
|
-
user_id: tokenData.user_id || tokenData.authed_user?.id,
|
|
115
|
-
scope: tokenData.scope || tokenData.authed_user?.scope,
|
|
116
|
-
obtained_at: new Date().toISOString(),
|
|
117
|
-
};
|
|
118
|
-
saveToken(tokenInfo);
|
|
119
|
-
|
|
120
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
121
|
-
res.end('<html><body style="font-family:system-ui;text-align:center;padding:40px"><h2>WALL-E connected to Slack!</h2><p>You can close this tab.</p></body></html>');
|
|
122
|
-
|
|
123
|
-
clearTimeout(timeout);
|
|
124
|
-
server.close();
|
|
125
|
-
resolve(tokenInfo.access_token);
|
|
126
|
-
} catch (err) {
|
|
127
|
-
res.writeHead(500);
|
|
128
|
-
res.end('Token exchange failed: ' + err.message);
|
|
129
|
-
clearTimeout(timeout);
|
|
130
|
-
server.close();
|
|
131
|
-
reject(err);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
64
|
+
_pendingOAuth = { verifier, state, redirectUri, createdAt: Date.now() };
|
|
134
65
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
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';
|
|
138
|
-
const authUrl = `https://slack.com/oauth/v2_user/authorize?client_id=${CLIENT_ID}&scope=${encodeURIComponent(scopes)}&redirect_uri=${encodeURIComponent(`http://localhost:${CALLBACK_PORT}/callback`)}&state=${state}&code_challenge=${challenge}&code_challenge_method=S256`;
|
|
139
|
-
console.log('[slack-mcp] Opening browser for Slack OAuth...');
|
|
140
|
-
console.log('[slack-mcp] Auth URL:', authUrl);
|
|
141
|
-
|
|
142
|
-
// Open browser
|
|
143
|
-
const { execFile } = require('child_process');
|
|
144
|
-
execFile('open', [authUrl], (err) => {
|
|
145
|
-
if (err) console.error('[slack-mcp] Failed to open browser:', err.message);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
66
|
+
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';
|
|
67
|
+
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`;
|
|
148
68
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
69
|
+
console.log('[slack-mcp] Opening browser for Slack OAuth...');
|
|
70
|
+
console.log('[slack-mcp] Callback URL:', redirectUri);
|
|
71
|
+
|
|
72
|
+
const { execFile } = require('child_process');
|
|
73
|
+
execFile('open', [authUrl], (err) => {
|
|
74
|
+
if (err) console.error('[slack-mcp] Failed to open browser:', err.message);
|
|
153
75
|
});
|
|
76
|
+
|
|
77
|
+
return null; // Token not yet available — will be set by callback
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Handle the OAuth callback from Slack (called by CTM server route).
|
|
82
|
+
* Returns { ok, html } or { error, html }.
|
|
83
|
+
*/
|
|
84
|
+
async function handleOAuthCallback(code, returnedState) {
|
|
85
|
+
if (!_pendingOAuth) {
|
|
86
|
+
return { error: 'No pending OAuth flow. Click "Connect" again.', html: errorPage('No pending OAuth flow. Go back and click Connect again.') };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Expire after 10 minutes
|
|
90
|
+
if (Date.now() - _pendingOAuth.createdAt > 600000) {
|
|
91
|
+
_pendingOAuth = null;
|
|
92
|
+
return { error: 'OAuth flow expired.', html: errorPage('OAuth flow expired. Go back and click Connect again.') };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (returnedState !== _pendingOAuth.state) {
|
|
96
|
+
_pendingOAuth = null;
|
|
97
|
+
return { error: 'State mismatch.', html: errorPage('OAuth state mismatch. Please try again.') };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!code) {
|
|
101
|
+
_pendingOAuth = null;
|
|
102
|
+
return { error: 'No code.', html: errorPage('No authorization code received from Slack.') };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const tokenResp = await fetch('https://slack.com/api/oauth.v2.user.access', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
109
|
+
body: new URLSearchParams({
|
|
110
|
+
client_id: CLIENT_ID,
|
|
111
|
+
code,
|
|
112
|
+
code_verifier: _pendingOAuth.verifier,
|
|
113
|
+
redirect_uri: _pendingOAuth.redirectUri,
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
const tokenData = await tokenResp.json();
|
|
117
|
+
|
|
118
|
+
_pendingOAuth = null;
|
|
119
|
+
|
|
120
|
+
if (!tokenData.ok) {
|
|
121
|
+
return { error: tokenData.error, html: errorPage('Slack OAuth error: ' + tokenData.error) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const tokenInfo = {
|
|
125
|
+
access_token: tokenData.access_token || tokenData.authed_user?.access_token,
|
|
126
|
+
refresh_token: tokenData.refresh_token,
|
|
127
|
+
team_id: tokenData.team?.id || tokenData.team_id,
|
|
128
|
+
team_name: tokenData.team?.name,
|
|
129
|
+
user_id: tokenData.user_id || tokenData.authed_user?.id,
|
|
130
|
+
scope: tokenData.scope || tokenData.authed_user?.scope,
|
|
131
|
+
obtained_at: new Date().toISOString(),
|
|
132
|
+
};
|
|
133
|
+
saveToken(tokenInfo);
|
|
134
|
+
|
|
135
|
+
console.log('[slack-mcp] OAuth completed — team:', tokenInfo.team_name);
|
|
136
|
+
return { ok: true, html: successPage() };
|
|
137
|
+
} catch (err) {
|
|
138
|
+
_pendingOAuth = null;
|
|
139
|
+
return { error: err.message, html: errorPage('Token exchange failed: ' + err.message) };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function successPage() {
|
|
144
|
+
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>';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function errorPage(msg) {
|
|
148
|
+
return '<html><body style="font-family:system-ui;text-align:center;padding:60px;background:#0d1117;color:#e6edf3"><h2 style="color:#f85149">Slack Connection Failed</h2><p style="color:#8b949e">' + msg + '</p></body></html>';
|
|
154
149
|
}
|
|
155
150
|
|
|
156
151
|
// ── MCP HTTP client ──
|
|
@@ -287,4 +282,4 @@ if (require.main === module) {
|
|
|
287
282
|
}
|
|
288
283
|
}
|
|
289
284
|
|
|
290
|
-
module.exports = { authenticate, callSlackMcp, listSlackTools, isAuthenticated, loadToken, clearToken };
|
|
285
|
+
module.exports = { authenticate, handleOAuthCallback, callSlackMcp, listSlackTools, isAuthenticated, loadToken, clearToken };
|