create-walle 0.2.1 → 0.3.1
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/bin/create-walle.js +3 -1
- package/package.json +1 -1
- package/template/claude-task-manager/public/index.html +2 -0
- package/template/claude-task-manager/public/setup.html +20 -4
- package/template/claude-task-manager/server.js +29 -9
- package/template/wall-e/api-walle.js +8 -6
- package/template/wall-e/tools/slack-mcp.js +97 -102
package/bin/create-walle.js
CHANGED
|
@@ -81,7 +81,8 @@ function install(targetDir) {
|
|
|
81
81
|
const ownerName = detectName().replace(/[\r\n=]/g, '').trim().slice(0, 200);
|
|
82
82
|
const timezone = detectTimezone();
|
|
83
83
|
const nameParts = ownerName.split(/\s+/);
|
|
84
|
-
const port = process.env.
|
|
84
|
+
const port = process.env.CTM_PORT || '3456';
|
|
85
|
+
const wallePort = String(parseInt(port) + 1);
|
|
85
86
|
|
|
86
87
|
console.log(` ${DIM}Owner: ${ownerName}${RESET}`);
|
|
87
88
|
console.log(` ${DIM}Timezone: ${timezone}${RESET}`);
|
|
@@ -110,6 +111,7 @@ function install(targetDir) {
|
|
|
110
111
|
'# ANTHROPIC_API_KEY=sk-ant-...',
|
|
111
112
|
'',
|
|
112
113
|
`CTM_PORT=${port}`,
|
|
114
|
+
`WALL_E_PORT=${wallePort}`,
|
|
113
115
|
'',
|
|
114
116
|
'# SLACK_TOKEN=',
|
|
115
117
|
'# SLACK_OWNER_USER_ID=',
|
package/package.json
CHANGED
|
@@ -3792,6 +3792,7 @@ function startRenameReviewTitle(titleEl) {
|
|
|
3792
3792
|
titleEl.textContent = newName;
|
|
3793
3793
|
renderFilteredSessions();
|
|
3794
3794
|
renderSessionList();
|
|
3795
|
+
renderTabs();
|
|
3795
3796
|
} else {
|
|
3796
3797
|
titleEl.textContent = currentText;
|
|
3797
3798
|
}
|
|
@@ -3836,6 +3837,7 @@ function startRenameRecentSession(sessionId, spanEl) {
|
|
|
3836
3837
|
}
|
|
3837
3838
|
renderFilteredSessions();
|
|
3838
3839
|
renderSessionList();
|
|
3840
|
+
renderTabs();
|
|
3839
3841
|
}
|
|
3840
3842
|
|
|
3841
3843
|
input.addEventListener('blur', finish);
|
|
@@ -197,11 +197,27 @@
|
|
|
197
197
|
btn.textContent = 'Connecting...';
|
|
198
198
|
const r = await fetch('/api/wall-e/slack/auth', { method: 'POST' });
|
|
199
199
|
const d = await r.json();
|
|
200
|
-
if (d.
|
|
201
|
-
window.open(d.url, '_blank');
|
|
202
|
-
btn.textContent = 'Check browser...';
|
|
203
|
-
} else if (d.ok) {
|
|
200
|
+
if (d.ok && d.already) {
|
|
204
201
|
btn.outerHTML = '<span class="badge badge-connected">Connected</span>';
|
|
202
|
+
} else if (d.ok) {
|
|
203
|
+
btn.textContent = 'Check browser...';
|
|
204
|
+
// Poll for completion (OAuth callback happens server-side)
|
|
205
|
+
let attempts = 0;
|
|
206
|
+
const poll = setInterval(async () => {
|
|
207
|
+
attempts++;
|
|
208
|
+
try {
|
|
209
|
+
const sr = await fetch('/api/setup/status');
|
|
210
|
+
const sd = await sr.json();
|
|
211
|
+
if (sd.slack_connected) {
|
|
212
|
+
clearInterval(poll);
|
|
213
|
+
btn.outerHTML = '<span class="badge badge-connected">Connected</span>';
|
|
214
|
+
} else if (attempts > 60) {
|
|
215
|
+
clearInterval(poll);
|
|
216
|
+
btn.textContent = 'Timed out';
|
|
217
|
+
btn.disabled = false;
|
|
218
|
+
}
|
|
219
|
+
} catch {}
|
|
220
|
+
}, 2000);
|
|
205
221
|
} else {
|
|
206
222
|
btn.textContent = d.error || 'Failed';
|
|
207
223
|
btn.disabled = false;
|
|
@@ -21,7 +21,7 @@ const approvalAgent = require('./approval-agent');
|
|
|
21
21
|
const { handleReviewApi, checkForChanges } = require('./api-reviews');
|
|
22
22
|
const { sessions } = require('./server-state');
|
|
23
23
|
|
|
24
|
-
// WALL-E API
|
|
24
|
+
// WALL-E API served directly by the WALL-E process (default: CTM_PORT + 1)
|
|
25
25
|
// Frontend connects to it via CORS. Keep proxy as fallback for environments where WALL-E isn't running separately.
|
|
26
26
|
let handleWalleApi;
|
|
27
27
|
try { handleWalleApi = require('../wall-e/api-walle').handleWalleApi; } catch {}
|
|
@@ -30,6 +30,7 @@ try { handleWalleApi = require('../wall-e/api-walle').handleWalleApi; } catch {}
|
|
|
30
30
|
const CONFIG_DIR = process.env.CTM_DATA_DIR || path.join(process.env.HOME, '.walle', 'data');
|
|
31
31
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
32
32
|
const PORT = parseInt(process.env.CTM_PORT || '3456', 10);
|
|
33
|
+
const WALLE_PORT = parseInt(process.env.WALL_E_PORT || String(PORT + 1), 10);
|
|
33
34
|
const HOST = process.env.CTM_HOST || '127.0.0.1';
|
|
34
35
|
|
|
35
36
|
function loadConfig() {
|
|
@@ -106,6 +107,26 @@ const server = http.createServer((req, res) => {
|
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
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
|
+
|
|
109
130
|
// API routes
|
|
110
131
|
if (url.pathname.startsWith('/api/')) {
|
|
111
132
|
if (!isLocalhost(req)) {
|
|
@@ -2096,8 +2117,7 @@ const _ctmStartTime = Date.now();
|
|
|
2096
2117
|
|
|
2097
2118
|
function apiServicesStatus(req, res) {
|
|
2098
2119
|
const ctmUptime = Math.floor((Date.now() - _ctmStartTime) / 1000);
|
|
2099
|
-
|
|
2100
|
-
execFile('lsof', ['-ti', ':3457'], (err, stdout) => {
|
|
2120
|
+
execFile('lsof', ['-ti', ':' + WALLE_PORT], (err, stdout) => {
|
|
2101
2121
|
const pids = (stdout || '').trim().split('\n').filter(Boolean);
|
|
2102
2122
|
// Filter to only node processes
|
|
2103
2123
|
let wallePid = null;
|
|
@@ -2114,7 +2134,7 @@ function apiServicesStatus(req, res) {
|
|
|
2114
2134
|
}
|
|
2115
2135
|
|
|
2116
2136
|
function apiStopWalle(req, res) {
|
|
2117
|
-
execFile('lsof', ['-ti', ':
|
|
2137
|
+
execFile('lsof', ['-ti', ':' + WALLE_PORT], (err, stdout) => {
|
|
2118
2138
|
const pids = (stdout || '').trim().split('\n').filter(Boolean);
|
|
2119
2139
|
if (pids.length === 0) {
|
|
2120
2140
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -2133,7 +2153,7 @@ function apiStartWalle(req, res) {
|
|
|
2133
2153
|
const walleDir = path.join(__dirname, '..', 'wall-e');
|
|
2134
2154
|
const agentScript = path.join(walleDir, 'agent.js');
|
|
2135
2155
|
// Check if already running
|
|
2136
|
-
execFile('lsof', ['-ti', ':
|
|
2156
|
+
execFile('lsof', ['-ti', ':' + WALLE_PORT], (err, stdout) => {
|
|
2137
2157
|
const pids = (stdout || '').trim().split('\n').filter(Boolean);
|
|
2138
2158
|
if (pids.length > 0) {
|
|
2139
2159
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -2143,7 +2163,7 @@ function apiStartWalle(req, res) {
|
|
|
2143
2163
|
const child = require('child_process').spawn(
|
|
2144
2164
|
process.execPath,
|
|
2145
2165
|
[agentScript],
|
|
2146
|
-
{ cwd: walleDir, detached: true, stdio: 'ignore', env: { ...process.env } }
|
|
2166
|
+
{ cwd: walleDir, detached: true, stdio: 'ignore', env: { ...process.env, WALL_E_PORT: String(WALLE_PORT) } }
|
|
2147
2167
|
);
|
|
2148
2168
|
child.unref();
|
|
2149
2169
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -2181,8 +2201,8 @@ function apiRestartWalle(req, res) {
|
|
|
2181
2201
|
const walleDir = path.join(__dirname, '..', 'wall-e');
|
|
2182
2202
|
const agentScript = path.join(walleDir, 'agent.js');
|
|
2183
2203
|
|
|
2184
|
-
// Kill existing Wall-E process
|
|
2185
|
-
execFile('lsof', ['-ti', ':
|
|
2204
|
+
// Kill existing Wall-E process
|
|
2205
|
+
execFile('lsof', ['-ti', ':' + WALLE_PORT], (err, stdout) => {
|
|
2186
2206
|
const pids = (stdout || '').trim().split('\n').filter(Boolean);
|
|
2187
2207
|
for (const pid of pids) {
|
|
2188
2208
|
try { process.kill(parseInt(pid), 'SIGTERM'); } catch {}
|
|
@@ -2193,7 +2213,7 @@ function apiRestartWalle(req, res) {
|
|
|
2193
2213
|
const child = require('child_process').spawn(
|
|
2194
2214
|
process.execPath,
|
|
2195
2215
|
[agentScript],
|
|
2196
|
-
{ cwd: walleDir, detached: true, stdio: 'ignore', env: { ...process.env } }
|
|
2216
|
+
{ cwd: walleDir, detached: true, stdio: 'ignore', env: { ...process.env, WALL_E_PORT: String(WALLE_PORT) } }
|
|
2197
2217
|
);
|
|
2198
2218
|
child.unref();
|
|
2199
2219
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -208,12 +208,14 @@ function handleWalleApi(req, res, url) {
|
|
|
208
208
|
if (p === '/api/wall-e/slack/auth' && m === 'POST') {
|
|
209
209
|
try {
|
|
210
210
|
const slackMcp = require('./tools/slack-mcp');
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
211
|
+
// If already authenticated, return immediately
|
|
212
|
+
if (slackMcp.isAuthenticated()) {
|
|
213
|
+
jsonResponse(res, { ok: true, already: true });
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
// Start OAuth — opens browser, callback handled by CTM server route
|
|
217
|
+
slackMcp.authenticate();
|
|
218
|
+
jsonResponse(res, { ok: true, pending: true });
|
|
217
219
|
} catch (e) {
|
|
218
220
|
jsonResponse(res, { error: e.message }, 500);
|
|
219
221
|
}
|
|
@@ -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 };
|