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.
@@ -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.WALLE_PORT || '4567';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-walle",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Set up Wall-E — your personal digital twin",
5
5
  "bin": {
6
6
  "create-walle": "bin/create-walle.js"
@@ -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.url) {
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 now served directly by the WALL-E process (port 3457)
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
- // Check Wall-E on port 3457
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', ':3457'], (err, stdout) => {
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', ':3457'], (err, stdout) => {
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 on port 3457
2185
- execFile('lsof', ['-ti', ':3457'], (err, stdout) => {
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
- slackMcp.authenticate().then(token => {
212
- console.log('[wall-e] Slack OAuth completed');
213
- }).catch(err => {
214
- console.error('[wall-e] Slack OAuth failed:', err.message);
215
- });
216
- jsonResponse(res, { data: { message: 'OAuth flow started check your browser' } });
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, waits for callback.
42
- * Returns the access token.
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
- async function authenticate() {
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
- return new Promise((resolve, reject) => {
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
- server.listen(CALLBACK_PORT, () => {
136
- // Scopes from Slack MCP's .well-known/oauth-authorization-server
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
- server.on('error', (err) => {
150
- clearTimeout(timeout);
151
- reject(new Error(`OAuth server failed: ${err.message}`));
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 };