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 +1 -1
- package/template/README.md +29 -1
- package/template/claude-task-manager/server.js +0 -20
- package/template/docs/site/guides/configuration.md +56 -26
- package/template/docs/site/src/content/docs/guides/configuration.md +56 -25
- package/template/wall-e/api-walle.js +6 -2
- package/template/wall-e/tools/slack-mcp.js +84 -90
package/package.json
CHANGED
package/template/README.md
CHANGED
|
@@ -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 |
|
|
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
|
-
###
|
|
12
|
+
### API Authentication
|
|
14
13
|
|
|
15
|
-
|
|
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
|
-
|
|
16
|
+
**Option A: Direct Anthropic API key**
|
|
21
17
|
|
|
22
|
-
|
|
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
|
-
|
|
20
|
+
```env
|
|
21
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
22
|
+
```
|
|
28
23
|
|
|
29
|
-
|
|
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
|
-
|
|
26
|
+
For teams using [Portkey](https://portkey.ai) or a corporate API gateway:
|
|
37
27
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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` |
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
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
|
-
###
|
|
17
|
+
### API Authentication
|
|
18
18
|
|
|
19
|
-
|
|
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
|
-
|
|
21
|
+
**Option A: Direct Anthropic API key**
|
|
25
22
|
|
|
26
|
-
|
|
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
|
-
|
|
25
|
+
```env
|
|
26
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
27
|
+
```
|
|
32
28
|
|
|
33
|
-
|
|
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
|
-
|
|
31
|
+
For teams using [Portkey](https://portkey.ai) or a corporate API gateway:
|
|
41
32
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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` |
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
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,
|
|
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:
|
|
50
|
-
*
|
|
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
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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,
|
|
278
|
+
module.exports = { authenticate, callSlackMcp, listSlackTools, isAuthenticated, loadToken, clearToken };
|