create-walle 0.4.2 → 0.4.4
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/public/setup.html +4 -0
- package/template/claude-task-manager/server.js +25 -21
- 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:
|
|
@@ -123,6 +123,7 @@
|
|
|
123
123
|
<div class="done-section">
|
|
124
124
|
<a href="/index.html" id="done-link">Go to Dashboard →</a>
|
|
125
125
|
<div style="margin-top:8px;font-size:12px;color:var(--dim)">You can return to this page anytime from the settings icon in the nav bar.</div>
|
|
126
|
+
<div id="version-label" style="margin-top:16px;font-size:11px;color:#30363d"></div>
|
|
126
127
|
</div>
|
|
127
128
|
</div>
|
|
128
129
|
|
|
@@ -139,6 +140,9 @@
|
|
|
139
140
|
if (d.slack_connected) {
|
|
140
141
|
document.getElementById('slack-btn').outerHTML = '<span class="badge badge-connected">Connected</span>';
|
|
141
142
|
}
|
|
143
|
+
if (d.version) {
|
|
144
|
+
document.getElementById('version-label').textContent = 'Wall-E v' + d.version;
|
|
145
|
+
}
|
|
142
146
|
} catch {}
|
|
143
147
|
}
|
|
144
148
|
|
|
@@ -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)) {
|
|
@@ -222,7 +202,9 @@ function handleApi(req, res, url) {
|
|
|
222
202
|
slackConnected = fs.existsSync(tokPath);
|
|
223
203
|
} catch {}
|
|
224
204
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
225
|
-
|
|
205
|
+
let version = '';
|
|
206
|
+
try { version = require('../package.json').version; } catch {}
|
|
207
|
+
res.end(JSON.stringify({ owner_name: ownerName, has_api_key: hasApiKey, slack_connected: slackConnected, needs_setup: setup.needsSetup(), version }));
|
|
226
208
|
return;
|
|
227
209
|
}
|
|
228
210
|
if (url.pathname === '/api/setup/detect-key' && req.method === 'GET') {
|
|
@@ -359,6 +341,10 @@ function handleApi(req, res, url) {
|
|
|
359
341
|
}
|
|
360
342
|
fs.writeFileSync(envPath, lines.join('\n') + '\n', { mode: 0o600 });
|
|
361
343
|
setup.clearSetupCache(); // so next / request goes to dashboard
|
|
344
|
+
// Restart Wall-E so it picks up the new env vars from .env
|
|
345
|
+
if (apiKey || gw) {
|
|
346
|
+
_restartWalleQuiet();
|
|
347
|
+
}
|
|
362
348
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
363
349
|
res.end(JSON.stringify({ ok: true }));
|
|
364
350
|
} catch (e) {
|
|
@@ -2201,6 +2187,24 @@ function apiStartWalle(req, res) {
|
|
|
2201
2187
|
});
|
|
2202
2188
|
}
|
|
2203
2189
|
|
|
2190
|
+
// Silent Wall-E restart (no HTTP response needed) — used after saving API key
|
|
2191
|
+
function _restartWalleQuiet() {
|
|
2192
|
+
const walleDir = path.join(__dirname, '..', 'wall-e');
|
|
2193
|
+
const agentScript = path.join(walleDir, 'agent.js');
|
|
2194
|
+
execFile('lsof', ['-ti', ':' + WALLE_PORT], (err, stdout) => {
|
|
2195
|
+
const pids = (stdout || '').trim().split('\n').filter(Boolean);
|
|
2196
|
+
for (const pid of pids) { try { process.kill(parseInt(pid), 'SIGTERM'); } catch {} }
|
|
2197
|
+
setTimeout(() => {
|
|
2198
|
+
const child = require('child_process').spawn(
|
|
2199
|
+
process.execPath, [agentScript],
|
|
2200
|
+
{ cwd: walleDir, detached: true, stdio: 'ignore', env: { ...process.env, WALL_E_PORT: String(WALLE_PORT) } }
|
|
2201
|
+
);
|
|
2202
|
+
child.unref();
|
|
2203
|
+
console.log('[setup] Restarted Wall-E (PID ' + child.pid + ') to pick up new API config');
|
|
2204
|
+
}, 1000);
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2204
2208
|
function apiRestartCtm(req, res) {
|
|
2205
2209
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2206
2210
|
res.end(JSON.stringify({ ok: true, message: 'CTM server restarting...' }));
|
|
@@ -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 };
|