@jtalk22/slack-mcp 1.0.0
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/LICENSE +21 -0
- package/README.md +295 -0
- package/docs/API.md +286 -0
- package/docs/SETUP.md +134 -0
- package/docs/TROUBLESHOOTING.md +216 -0
- package/docs/WEB-API.md +277 -0
- package/docs/images/demo-channel-messages.png +0 -0
- package/docs/images/demo-channels.png +0 -0
- package/docs/images/demo-main.png +0 -0
- package/docs/images/demo-messages.png +0 -0
- package/docs/images/demo-sidebar.png +0 -0
- package/lib/handlers.js +421 -0
- package/lib/slack-client.js +119 -0
- package/lib/token-store.js +184 -0
- package/lib/tools.js +191 -0
- package/package.json +70 -0
- package/public/demo.html +920 -0
- package/public/index.html +258 -0
- package/scripts/capture-screenshots.js +96 -0
- package/scripts/publish-public.sh +37 -0
- package/scripts/sync-from-onedrive.sh +33 -0
- package/scripts/sync-to-onedrive.sh +31 -0
- package/scripts/token-cli.js +157 -0
- package/src/server.js +118 -0
- package/src/web-server.js +256 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Slack Web API</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
11
|
+
background: #1a1a2e;
|
|
12
|
+
color: #eee;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
padding: 20px;
|
|
15
|
+
}
|
|
16
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
17
|
+
h1 { color: #4ecdc4; margin-bottom: 20px; }
|
|
18
|
+
.auth-section {
|
|
19
|
+
background: #16213e;
|
|
20
|
+
padding: 20px;
|
|
21
|
+
border-radius: 8px;
|
|
22
|
+
margin-bottom: 20px;
|
|
23
|
+
}
|
|
24
|
+
.auth-section input {
|
|
25
|
+
width: 300px;
|
|
26
|
+
padding: 10px;
|
|
27
|
+
border: none;
|
|
28
|
+
border-radius: 4px;
|
|
29
|
+
background: #0f3460;
|
|
30
|
+
color: #fff;
|
|
31
|
+
margin-right: 10px;
|
|
32
|
+
}
|
|
33
|
+
.auth-section button {
|
|
34
|
+
padding: 10px 20px;
|
|
35
|
+
background: #4ecdc4;
|
|
36
|
+
color: #1a1a2e;
|
|
37
|
+
border: none;
|
|
38
|
+
border-radius: 4px;
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
font-weight: bold;
|
|
41
|
+
}
|
|
42
|
+
.auth-section button:hover { background: #45b7aa; }
|
|
43
|
+
.status { display: inline-block; margin-left: 15px; }
|
|
44
|
+
.status.ok { color: #4ecdc4; }
|
|
45
|
+
.status.error { color: #ff6b6b; }
|
|
46
|
+
.grid { display: grid; grid-template-columns: 300px 1fr; gap: 20px; }
|
|
47
|
+
.sidebar {
|
|
48
|
+
background: #16213e;
|
|
49
|
+
border-radius: 8px;
|
|
50
|
+
padding: 15px;
|
|
51
|
+
height: fit-content;
|
|
52
|
+
}
|
|
53
|
+
.sidebar h3 { color: #4ecdc4; margin-bottom: 15px; font-size: 14px; }
|
|
54
|
+
.conversation-list { list-style: none; }
|
|
55
|
+
.conversation-list li {
|
|
56
|
+
padding: 10px;
|
|
57
|
+
border-radius: 4px;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
margin-bottom: 5px;
|
|
60
|
+
background: #0f3460;
|
|
61
|
+
}
|
|
62
|
+
.conversation-list li:hover { background: #1a4a7a; }
|
|
63
|
+
.conversation-list li.active { background: #4ecdc4; color: #1a1a2e; }
|
|
64
|
+
.conversation-list .type { font-size: 11px; opacity: 0.7; }
|
|
65
|
+
.main-panel {
|
|
66
|
+
background: #16213e;
|
|
67
|
+
border-radius: 8px;
|
|
68
|
+
padding: 20px;
|
|
69
|
+
min-height: 500px;
|
|
70
|
+
}
|
|
71
|
+
.main-panel h2 { color: #4ecdc4; margin-bottom: 15px; }
|
|
72
|
+
.messages { max-height: 400px; overflow-y: auto; margin-bottom: 15px; }
|
|
73
|
+
.message {
|
|
74
|
+
background: #0f3460;
|
|
75
|
+
padding: 12px;
|
|
76
|
+
border-radius: 8px;
|
|
77
|
+
margin-bottom: 10px;
|
|
78
|
+
}
|
|
79
|
+
.message .header { display: flex; justify-content: space-between; margin-bottom: 8px; }
|
|
80
|
+
.message .user { font-weight: bold; color: #4ecdc4; }
|
|
81
|
+
.message .time { font-size: 12px; opacity: 0.6; }
|
|
82
|
+
.message .text { line-height: 1.5; white-space: pre-wrap; }
|
|
83
|
+
.send-box { display: flex; gap: 10px; }
|
|
84
|
+
.send-box input {
|
|
85
|
+
flex: 1;
|
|
86
|
+
padding: 12px;
|
|
87
|
+
border: none;
|
|
88
|
+
border-radius: 4px;
|
|
89
|
+
background: #0f3460;
|
|
90
|
+
color: #fff;
|
|
91
|
+
}
|
|
92
|
+
.send-box button {
|
|
93
|
+
padding: 12px 24px;
|
|
94
|
+
background: #4ecdc4;
|
|
95
|
+
color: #1a1a2e;
|
|
96
|
+
border: none;
|
|
97
|
+
border-radius: 4px;
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
font-weight: bold;
|
|
100
|
+
}
|
|
101
|
+
.search-section { margin-bottom: 20px; display: flex; gap: 10px; }
|
|
102
|
+
.search-section input {
|
|
103
|
+
flex: 1;
|
|
104
|
+
padding: 10px;
|
|
105
|
+
border: none;
|
|
106
|
+
border-radius: 4px;
|
|
107
|
+
background: #0f3460;
|
|
108
|
+
color: #fff;
|
|
109
|
+
}
|
|
110
|
+
.search-section button {
|
|
111
|
+
padding: 10px 20px;
|
|
112
|
+
background: #e94560;
|
|
113
|
+
color: #fff;
|
|
114
|
+
border: none;
|
|
115
|
+
border-radius: 4px;
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
}
|
|
118
|
+
.loading { text-align: center; padding: 40px; opacity: 0.6; }
|
|
119
|
+
.error-msg { background: #ff6b6b22; color: #ff6b6b; padding: 15px; border-radius: 8px; }
|
|
120
|
+
.tabs { display: flex; gap: 5px; margin-bottom: 15px; }
|
|
121
|
+
.tabs button {
|
|
122
|
+
padding: 8px 16px;
|
|
123
|
+
background: #0f3460;
|
|
124
|
+
color: #fff;
|
|
125
|
+
border: none;
|
|
126
|
+
border-radius: 4px;
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
}
|
|
129
|
+
.tabs button.active { background: #4ecdc4; color: #1a1a2e; }
|
|
130
|
+
</style>
|
|
131
|
+
</head>
|
|
132
|
+
<body>
|
|
133
|
+
<div class="container">
|
|
134
|
+
<h1>Slack Web API</h1>
|
|
135
|
+
<div class="auth-section">
|
|
136
|
+
<input type="text" id="apiKey" placeholder="API Key (default: slack-mcp-local)">
|
|
137
|
+
<button onclick="connect()">Connect</button>
|
|
138
|
+
<span id="status" class="status"></span>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="grid">
|
|
141
|
+
<div class="sidebar">
|
|
142
|
+
<h3>CONVERSATIONS</h3>
|
|
143
|
+
<div class="tabs">
|
|
144
|
+
<button class="active" onclick="loadConversations('im,mpim')">DMs</button>
|
|
145
|
+
<button onclick="loadConversations('public_channel,private_channel')">Channels</button>
|
|
146
|
+
</div>
|
|
147
|
+
<ul id="conversationList" class="conversation-list">
|
|
148
|
+
<li class="loading">Enter API key to connect</li>
|
|
149
|
+
</ul>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="main-panel">
|
|
152
|
+
<div class="search-section">
|
|
153
|
+
<input type="text" id="searchQuery" placeholder="Search messages...">
|
|
154
|
+
<button onclick="searchMessages()">Search</button>
|
|
155
|
+
</div>
|
|
156
|
+
<h2 id="channelName">Select a conversation</h2>
|
|
157
|
+
<div id="messages" class="messages">
|
|
158
|
+
<div class="loading">Messages will appear here</div>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="send-box">
|
|
161
|
+
<input type="text" id="messageInput" placeholder="Type a message..." onkeypress="if(event.key==='Enter')sendMessage()">
|
|
162
|
+
<button onclick="sendMessage()">Send</button>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
<script>
|
|
168
|
+
// Default API key matches server default - auto-connects on first visit
|
|
169
|
+
const DEFAULT_KEY = 'slack-mcp-local';
|
|
170
|
+
let apiKey = localStorage.getItem('slackApiKey') || DEFAULT_KEY;
|
|
171
|
+
let currentChannel = null;
|
|
172
|
+
|
|
173
|
+
document.getElementById('apiKey').value = apiKey;
|
|
174
|
+
setTimeout(connect, 100);
|
|
175
|
+
async function api(endpoint, options = {}) {
|
|
176
|
+
const res = await fetch(endpoint, {
|
|
177
|
+
...options,
|
|
178
|
+
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json', ...options.headers }
|
|
179
|
+
});
|
|
180
|
+
if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'API error'); }
|
|
181
|
+
return res.json();
|
|
182
|
+
}
|
|
183
|
+
async function connect() {
|
|
184
|
+
apiKey = document.getElementById('apiKey').value.trim() || DEFAULT_KEY;
|
|
185
|
+
localStorage.setItem('slackApiKey', apiKey);
|
|
186
|
+
const status = document.getElementById('status');
|
|
187
|
+
try {
|
|
188
|
+
const health = await api('/health');
|
|
189
|
+
status.textContent = 'Connected as ' + health.user + ' @ ' + health.team;
|
|
190
|
+
status.className = 'status ok';
|
|
191
|
+
loadConversations('im,mpim');
|
|
192
|
+
} catch (e) {
|
|
193
|
+
status.textContent = 'Error: ' + e.message;
|
|
194
|
+
status.className = 'status error';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function loadConversations(types) {
|
|
198
|
+
const list = document.getElementById('conversationList');
|
|
199
|
+
list.innerHTML = '<li class="loading">Loading...</li>';
|
|
200
|
+
document.querySelectorAll('.tabs button').forEach(b => b.classList.remove('active'));
|
|
201
|
+
if (event && event.target) event.target.classList.add('active');
|
|
202
|
+
try {
|
|
203
|
+
const data = await api('/conversations?types=' + types);
|
|
204
|
+
list.innerHTML = data.conversations.map(c =>
|
|
205
|
+
'<li onclick="loadHistory(\'' + c.id + '\', \'' + c.name.replace(/'/g, "\\'") + '\')"><div>' + c.name + '</div><div class="type">' + c.type + '</div></li>'
|
|
206
|
+
).join('');
|
|
207
|
+
} catch (e) { list.innerHTML = '<li class="error-msg">' + e.message + '</li>'; }
|
|
208
|
+
}
|
|
209
|
+
async function loadHistory(channelId, name) {
|
|
210
|
+
currentChannel = channelId;
|
|
211
|
+
document.getElementById('channelName').textContent = name;
|
|
212
|
+
document.querySelectorAll('.conversation-list li').forEach(li => li.classList.remove('active'));
|
|
213
|
+
event.target.closest('li').classList.add('active');
|
|
214
|
+
const container = document.getElementById('messages');
|
|
215
|
+
container.innerHTML = '<div class="loading">Loading messages...</div>';
|
|
216
|
+
try {
|
|
217
|
+
const data = await api('/conversations/' + channelId + '/history?limit=50');
|
|
218
|
+
renderMessages(data.messages);
|
|
219
|
+
} catch (e) { container.innerHTML = '<div class="error-msg">' + e.message + '</div>'; }
|
|
220
|
+
}
|
|
221
|
+
function renderMessages(messages) {
|
|
222
|
+
const container = document.getElementById('messages');
|
|
223
|
+
if (!messages || messages.length === 0) { container.innerHTML = '<div class="loading">No messages</div>'; return; }
|
|
224
|
+
container.innerHTML = messages.map(m =>
|
|
225
|
+
'<div class="message"><div class="header"><span class="user">' + (m.user || m.user_id || 'Unknown') + '</span><span class="time">' + new Date(m.datetime).toLocaleString() + '</span></div><div class="text">' + escapeHtml(m.text || '') + '</div></div>'
|
|
226
|
+
).join('');
|
|
227
|
+
container.scrollTop = container.scrollHeight;
|
|
228
|
+
}
|
|
229
|
+
function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }
|
|
230
|
+
async function sendMessage() {
|
|
231
|
+
if (!currentChannel) { alert('Select a conversation first'); return; }
|
|
232
|
+
const input = document.getElementById('messageInput');
|
|
233
|
+
const text = input.value.trim();
|
|
234
|
+
if (!text) return;
|
|
235
|
+
try {
|
|
236
|
+
await api('/messages', { method: 'POST', body: JSON.stringify({ channel_id: currentChannel, text: text }) });
|
|
237
|
+
input.value = '';
|
|
238
|
+
loadHistory(currentChannel, document.getElementById('channelName').textContent);
|
|
239
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
240
|
+
}
|
|
241
|
+
async function searchMessages() {
|
|
242
|
+
const query = document.getElementById('searchQuery').value.trim();
|
|
243
|
+
if (!query) return;
|
|
244
|
+
document.getElementById('channelName').textContent = 'Search: "' + query + '"';
|
|
245
|
+
const container = document.getElementById('messages');
|
|
246
|
+
container.innerHTML = '<div class="loading">Searching...</div>';
|
|
247
|
+
try {
|
|
248
|
+
const data = await api('/search?q=' + encodeURIComponent(query));
|
|
249
|
+
if (data.matches && data.matches.length > 0) {
|
|
250
|
+
container.innerHTML = data.matches.map(m =>
|
|
251
|
+
'<div class="message"><div class="header"><span class="user">' + (m.user || 'Unknown') + ' in #' + m.channel + '</span><span class="time">' + new Date(m.datetime).toLocaleString() + '</span></div><div class="text">' + escapeHtml(m.text || '') + '</div></div>'
|
|
252
|
+
).join('');
|
|
253
|
+
} else { container.innerHTML = '<div class="loading">No results found</div>'; }
|
|
254
|
+
} catch (e) { container.innerHTML = '<div class="error-msg">' + e.message + '</div>'; }
|
|
255
|
+
}
|
|
256
|
+
</script>
|
|
257
|
+
</body>
|
|
258
|
+
</html>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Screenshot capture script using Playwright
|
|
4
|
+
* Captures polished screenshots of the demo UI for README/docs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { chromium } from 'playwright';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { dirname, join } from 'path';
|
|
10
|
+
import { readFileSync } from 'fs';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
const projectRoot = join(__dirname, '..');
|
|
15
|
+
|
|
16
|
+
async function captureScreenshots() {
|
|
17
|
+
console.log('Launching browser...');
|
|
18
|
+
|
|
19
|
+
const browser = await chromium.launch({
|
|
20
|
+
headless: true
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const context = await browser.newContext({
|
|
24
|
+
viewport: { width: 1400, height: 900 },
|
|
25
|
+
deviceScaleFactor: 2, // Retina quality
|
|
26
|
+
colorScheme: 'dark'
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const page = await context.newPage();
|
|
30
|
+
|
|
31
|
+
// Load the demo.html file directly
|
|
32
|
+
const demoPath = join(projectRoot, 'public', 'demo.html');
|
|
33
|
+
const demoHtml = readFileSync(demoPath, 'utf-8');
|
|
34
|
+
|
|
35
|
+
// Serve it as a data URL or file URL
|
|
36
|
+
await page.goto(`file://${demoPath}`);
|
|
37
|
+
|
|
38
|
+
// Wait for content to render
|
|
39
|
+
await page.waitForTimeout(1000);
|
|
40
|
+
|
|
41
|
+
const imagesDir = join(projectRoot, 'docs', 'images');
|
|
42
|
+
|
|
43
|
+
// Screenshot 1: Full UI with DMs
|
|
44
|
+
console.log('Capturing main UI screenshot...');
|
|
45
|
+
await page.screenshot({
|
|
46
|
+
path: join(imagesDir, 'demo-main.png'),
|
|
47
|
+
clip: { x: 0, y: 0, width: 1400, height: 800 }
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Screenshot 2: Conversation view (zoomed)
|
|
51
|
+
console.log('Capturing conversation screenshot...');
|
|
52
|
+
const mainPanel = await page.$('.main-panel');
|
|
53
|
+
if (mainPanel) {
|
|
54
|
+
await mainPanel.screenshot({
|
|
55
|
+
path: join(imagesDir, 'demo-messages.png')
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Screenshot 3: Sidebar with conversations
|
|
60
|
+
console.log('Capturing sidebar screenshot...');
|
|
61
|
+
const sidebar = await page.$('.sidebar');
|
|
62
|
+
if (sidebar) {
|
|
63
|
+
await sidebar.screenshot({
|
|
64
|
+
path: join(imagesDir, 'demo-sidebar.png')
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Screenshot 4: Switch to channels view
|
|
69
|
+
console.log('Capturing channels view...');
|
|
70
|
+
await page.click('.tabs button:nth-child(2)'); // Click Channels tab
|
|
71
|
+
await page.waitForTimeout(300);
|
|
72
|
+
await page.screenshot({
|
|
73
|
+
path: join(imagesDir, 'demo-channels.png'),
|
|
74
|
+
clip: { x: 0, y: 0, width: 1400, height: 800 }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Screenshot 5: Engineering channel messages
|
|
78
|
+
console.log('Capturing channel messages...');
|
|
79
|
+
await page.click('.conversation-item:first-child'); // Click first channel
|
|
80
|
+
await page.waitForTimeout(300);
|
|
81
|
+
await page.screenshot({
|
|
82
|
+
path: join(imagesDir, 'demo-channel-messages.png'),
|
|
83
|
+
clip: { x: 0, y: 0, width: 1400, height: 800 }
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await browser.close();
|
|
87
|
+
|
|
88
|
+
console.log('\nScreenshots saved to docs/images/');
|
|
89
|
+
console.log(' - demo-main.png');
|
|
90
|
+
console.log(' - demo-messages.png');
|
|
91
|
+
console.log(' - demo-sidebar.png');
|
|
92
|
+
console.log(' - demo-channels.png');
|
|
93
|
+
console.log(' - demo-channel-messages.png');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
captureScreenshots().catch(console.error);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Sync improvements from private to public and prepare for publishing
|
|
3
|
+
# Usage: ./scripts/publish-public.sh
|
|
4
|
+
|
|
5
|
+
PRIVATE="$HOME/slack-mcp-server"
|
|
6
|
+
PUBLIC="$HOME/slack-mcp-server-public"
|
|
7
|
+
|
|
8
|
+
echo "Syncing improvements from private → public..."
|
|
9
|
+
echo ""
|
|
10
|
+
|
|
11
|
+
# Files to sync (code only, not configs)
|
|
12
|
+
FILES_TO_SYNC=(
|
|
13
|
+
"lib/handlers.js"
|
|
14
|
+
"lib/slack-client.js"
|
|
15
|
+
"lib/token-store.js"
|
|
16
|
+
"lib/tools.js"
|
|
17
|
+
"scripts/token-cli.js"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
for file in "${FILES_TO_SYNC[@]}"; do
|
|
21
|
+
if [[ -f "$PRIVATE/$file" ]]; then
|
|
22
|
+
echo " → $file"
|
|
23
|
+
cp "$PRIVATE/$file" "$PUBLIC/$file"
|
|
24
|
+
fi
|
|
25
|
+
done
|
|
26
|
+
|
|
27
|
+
echo ""
|
|
28
|
+
echo "Synced core files. Review changes:"
|
|
29
|
+
echo ""
|
|
30
|
+
cd "$PUBLIC" && git status
|
|
31
|
+
|
|
32
|
+
echo ""
|
|
33
|
+
echo "Next steps:"
|
|
34
|
+
echo " 1. Review: cd ~/slack-mcp-server-public && git diff"
|
|
35
|
+
echo " 2. Commit: git add -A && git commit -m 'Your message'"
|
|
36
|
+
echo " 3. Push: git push origin main"
|
|
37
|
+
echo " 4. npm: npm publish (optional)"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Restore local slack-mcp-server from OneDrive backup
|
|
3
|
+
# Usage: ./scripts/sync-from-onedrive.sh [private|public|both]
|
|
4
|
+
|
|
5
|
+
ONEDRIVE_BASE="$HOME/Library/CloudStorage/OneDrive-Personal/Development/slack-mcp-server"
|
|
6
|
+
TARGET="${1:-both}"
|
|
7
|
+
|
|
8
|
+
echo "Restoring Slack MCP Server from OneDrive..."
|
|
9
|
+
|
|
10
|
+
if [[ "$TARGET" == "private" || "$TARGET" == "both" ]]; then
|
|
11
|
+
echo " → Restoring private version..."
|
|
12
|
+
rsync -av --delete \
|
|
13
|
+
--exclude 'node_modules' \
|
|
14
|
+
--exclude '.git' \
|
|
15
|
+
"$ONEDRIVE_BASE/private/" \
|
|
16
|
+
"$HOME/slack-mcp-server/"
|
|
17
|
+
echo " → Installing dependencies..."
|
|
18
|
+
cd "$HOME/slack-mcp-server" && npm install
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
if [[ "$TARGET" == "public" || "$TARGET" == "both" ]]; then
|
|
22
|
+
echo " → Restoring public version..."
|
|
23
|
+
rsync -av --delete \
|
|
24
|
+
--exclude 'node_modules' \
|
|
25
|
+
--exclude '.git' \
|
|
26
|
+
"$ONEDRIVE_BASE/public/" \
|
|
27
|
+
"$HOME/slack-mcp-server-public/"
|
|
28
|
+
echo " → Installing dependencies..."
|
|
29
|
+
cd "$HOME/slack-mcp-server-public" && npm install
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
echo ""
|
|
33
|
+
echo "Done! Restored from: $ONEDRIVE_BASE"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Sync local slack-mcp-server to OneDrive backup
|
|
3
|
+
# Usage: ./scripts/sync-to-onedrive.sh
|
|
4
|
+
|
|
5
|
+
ONEDRIVE_BASE="$HOME/Library/CloudStorage/OneDrive-Personal/Development/slack-mcp-server"
|
|
6
|
+
|
|
7
|
+
echo "Syncing Slack MCP Server to OneDrive..."
|
|
8
|
+
|
|
9
|
+
# Sync private version (exclude node_modules and .git)
|
|
10
|
+
echo " → Syncing private version..."
|
|
11
|
+
rsync -av --delete \
|
|
12
|
+
--exclude 'node_modules' \
|
|
13
|
+
--exclude '.git' \
|
|
14
|
+
--exclude '*.log' \
|
|
15
|
+
"$HOME/slack-mcp-server/" \
|
|
16
|
+
"$ONEDRIVE_BASE/private/"
|
|
17
|
+
|
|
18
|
+
# Sync public version (exclude node_modules and .git)
|
|
19
|
+
echo " → Syncing public version..."
|
|
20
|
+
rsync -av --delete \
|
|
21
|
+
--exclude 'node_modules' \
|
|
22
|
+
--exclude '.git' \
|
|
23
|
+
--exclude '*.log' \
|
|
24
|
+
"$HOME/slack-mcp-server-public/" \
|
|
25
|
+
"$ONEDRIVE_BASE/public/"
|
|
26
|
+
|
|
27
|
+
echo ""
|
|
28
|
+
echo "Done! Synced to: $ONEDRIVE_BASE"
|
|
29
|
+
echo ""
|
|
30
|
+
echo "Contents:"
|
|
31
|
+
ls -la "$ONEDRIVE_BASE"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Token CLI - Manage Slack tokens
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { loadTokens, saveTokens, extractFromChrome, getFromFile, TOKEN_FILE } from "../lib/token-store.js";
|
|
7
|
+
import { slackAPI } from "../lib/slack-client.js";
|
|
8
|
+
import * as readline from "readline";
|
|
9
|
+
|
|
10
|
+
const command = process.argv[2];
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
switch (command) {
|
|
14
|
+
case "status":
|
|
15
|
+
await showStatus();
|
|
16
|
+
break;
|
|
17
|
+
case "refresh":
|
|
18
|
+
await manualRefresh();
|
|
19
|
+
break;
|
|
20
|
+
case "auto":
|
|
21
|
+
await autoExtract();
|
|
22
|
+
break;
|
|
23
|
+
case "clear":
|
|
24
|
+
await clearTokens();
|
|
25
|
+
break;
|
|
26
|
+
default:
|
|
27
|
+
console.log("Usage: token-cli.js <command>");
|
|
28
|
+
console.log("");
|
|
29
|
+
console.log("Commands:");
|
|
30
|
+
console.log(" status Check current token status");
|
|
31
|
+
console.log(" refresh Manually enter new tokens");
|
|
32
|
+
console.log(" auto Auto-extract from Chrome");
|
|
33
|
+
console.log(" clear Remove all stored tokens");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function showStatus() {
|
|
38
|
+
const creds = loadTokens();
|
|
39
|
+
if (!creds) {
|
|
40
|
+
console.log("No tokens found");
|
|
41
|
+
console.log("");
|
|
42
|
+
console.log("Run one of:");
|
|
43
|
+
console.log(" npm run tokens:auto (with Slack open in Chrome)");
|
|
44
|
+
console.log(" npm run tokens:refresh (manual entry)");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log("Token source:", creds.source);
|
|
49
|
+
console.log("Token file:", TOKEN_FILE);
|
|
50
|
+
console.log("");
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const result = await slackAPI("auth.test", {});
|
|
54
|
+
console.log("Status: VALID");
|
|
55
|
+
console.log("User:", result.user);
|
|
56
|
+
console.log("Team:", result.team);
|
|
57
|
+
console.log("User ID:", result.user_id);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.log("Status: INVALID");
|
|
60
|
+
console.log("Error:", e.message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function manualRefresh() {
|
|
65
|
+
const rl = readline.createInterface({
|
|
66
|
+
input: process.stdin,
|
|
67
|
+
output: process.stdout
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log("Manual Token Entry");
|
|
71
|
+
console.log("==================");
|
|
72
|
+
console.log("");
|
|
73
|
+
console.log("Get tokens from Chrome DevTools:");
|
|
74
|
+
console.log("1. Open https://app.slack.com");
|
|
75
|
+
console.log("2. F12 → Application → Cookies → find 'd' cookie");
|
|
76
|
+
console.log("3. F12 → Console → run:");
|
|
77
|
+
console.log(" JSON.parse(localStorage.localConfig_v2).teams[Object.keys(JSON.parse(localStorage.localConfig_v2).teams)[0]].token");
|
|
78
|
+
console.log("");
|
|
79
|
+
|
|
80
|
+
const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
|
|
81
|
+
|
|
82
|
+
const token = await question("Enter XOXC token: ");
|
|
83
|
+
const cookie = await question("Enter XOXD cookie: ");
|
|
84
|
+
|
|
85
|
+
rl.close();
|
|
86
|
+
|
|
87
|
+
if (!token.startsWith("xoxc-") || !cookie.startsWith("xoxd-")) {
|
|
88
|
+
console.log("");
|
|
89
|
+
console.log("Invalid tokens. Token should start with xoxc-, cookie with xoxd-");
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
saveTokens(token, cookie);
|
|
94
|
+
console.log("");
|
|
95
|
+
console.log("Tokens saved!");
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const result = await slackAPI("auth.test", {});
|
|
99
|
+
console.log("Verified as:", result.user, "@", result.team);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.log("Warning: Tokens may be invalid:", e.message);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function autoExtract() {
|
|
106
|
+
console.log("Attempting Chrome auto-extraction...");
|
|
107
|
+
console.log("");
|
|
108
|
+
console.log("Make sure:");
|
|
109
|
+
console.log(" - Chrome is running");
|
|
110
|
+
console.log(" - Slack tab is open (app.slack.com)");
|
|
111
|
+
console.log(" - You're logged in");
|
|
112
|
+
console.log("");
|
|
113
|
+
|
|
114
|
+
const tokens = extractFromChrome();
|
|
115
|
+
if (!tokens) {
|
|
116
|
+
console.log("Failed to extract tokens from Chrome.");
|
|
117
|
+
console.log("Try manual entry: npm run tokens:refresh");
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
saveTokens(tokens.token, tokens.cookie);
|
|
122
|
+
console.log("Tokens extracted and saved!");
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const result = await slackAPI("auth.test", {});
|
|
126
|
+
console.log("Verified as:", result.user, "@", result.team);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.log("Warning: Tokens may be invalid:", e.message);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function clearTokens() {
|
|
133
|
+
const fs = await import("fs");
|
|
134
|
+
const { execSync } = await import("child_process");
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
fs.unlinkSync(TOKEN_FILE);
|
|
138
|
+
console.log("Deleted token file");
|
|
139
|
+
} catch (e) {
|
|
140
|
+
console.log("No token file to delete");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
execSync('security delete-generic-password -s "slack-mcp-server" -a "token" 2>/dev/null');
|
|
145
|
+
execSync('security delete-generic-password -s "slack-mcp-server" -a "cookie" 2>/dev/null');
|
|
146
|
+
console.log("Deleted keychain entries");
|
|
147
|
+
} catch (e) {
|
|
148
|
+
console.log("No keychain entries to delete");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log("All tokens cleared");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
main().catch(e => {
|
|
155
|
+
console.error("Error:", e.message);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
});
|