@integrity-labs/agt-cli 0.8.9 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/agt.js +3 -3
- package/dist/{chunk-55TMBRXT.js → chunk-AMB6FJLZ.js} +12 -1
- package/dist/chunk-AMB6FJLZ.js.map +1 -0
- package/dist/chunk-HTBIKZKS.js +455 -0
- package/dist/chunk-HTBIKZKS.js.map +1 -0
- package/dist/lib/manager-worker.js +192 -501
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/persistent-session-ZCB562FN.js +25 -0
- package/dist/persistent-session-ZCB562FN.js.map +1 -0
- package/mcp/index.js +587 -0
- package/mcp/slack-channel.js +254 -0
- package/package.json +4 -3
- package/dist/chunk-55TMBRXT.js.map +0 -1
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Slack channel MCP server for Claude Code persistent sessions.
|
|
4
|
+
*
|
|
5
|
+
* Declares `claude/channel` capability so inbound Slack messages are pushed
|
|
6
|
+
* directly into the Claude Code session. Uses Socket Mode (WebSocket) — no
|
|
7
|
+
* public URL needed.
|
|
8
|
+
*
|
|
9
|
+
* Env vars (passed via .mcp.json env block, per-agent):
|
|
10
|
+
* SLACK_BOT_TOKEN — Bot User OAuth Token (xoxb-...)
|
|
11
|
+
* SLACK_APP_TOKEN — App-Level Token for Socket Mode (xapp-...)
|
|
12
|
+
* SLACK_ALLOWED_USERS — Comma-separated Slack user IDs for sender gating (optional)
|
|
13
|
+
*/
|
|
14
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
15
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
16
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
17
|
+
const BOT_TOKEN = process.env.SLACK_BOT_TOKEN;
|
|
18
|
+
const APP_TOKEN = process.env.SLACK_APP_TOKEN;
|
|
19
|
+
const ALLOWED_USERS = new Set((process.env.SLACK_ALLOWED_USERS ?? '')
|
|
20
|
+
.split(',')
|
|
21
|
+
.map((s) => s.trim())
|
|
22
|
+
.filter(Boolean));
|
|
23
|
+
if (!BOT_TOKEN || !APP_TOKEN) {
|
|
24
|
+
console.error('slack-channel: Missing SLACK_BOT_TOKEN or SLACK_APP_TOKEN. Cannot start.');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// MCP Server with channel capability
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const mcp = new Server({ name: 'slack', version: '0.1.0' }, {
|
|
31
|
+
capabilities: {
|
|
32
|
+
experimental: { 'claude/channel': {} },
|
|
33
|
+
tools: {},
|
|
34
|
+
},
|
|
35
|
+
instructions: [
|
|
36
|
+
'Messages from Slack arrive as <channel source="slack" user="..." channel="..." thread_ts="...">.',
|
|
37
|
+
'Reply using the slack.reply tool, passing channel and thread_ts from the tag.',
|
|
38
|
+
'For threaded replies, always include thread_ts so the response appears in the same thread.',
|
|
39
|
+
'When someone @mentions you in a channel, respond helpfully in that thread.',
|
|
40
|
+
'For DMs, respond directly.',
|
|
41
|
+
].join(' '),
|
|
42
|
+
});
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Reply tool — Claude calls this to send messages back to Slack
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
47
|
+
tools: [
|
|
48
|
+
{
|
|
49
|
+
name: 'slack.reply',
|
|
50
|
+
description: 'Send a message back to a Slack channel or thread',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
channel: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'Slack channel ID (from the channel attribute in the <channel> tag)',
|
|
57
|
+
},
|
|
58
|
+
text: { type: 'string', description: 'The message to send' },
|
|
59
|
+
thread_ts: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: 'Thread timestamp for threaded replies (from the thread_ts attribute)',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
required: ['channel', 'text'],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'slack.react',
|
|
69
|
+
description: 'Add an emoji reaction to a Slack message',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
channel: { type: 'string', description: 'Slack channel ID' },
|
|
74
|
+
timestamp: { type: 'string', description: 'Message timestamp to react to' },
|
|
75
|
+
emoji: { type: 'string', description: 'Emoji name without colons (e.g. "thumbsup")' },
|
|
76
|
+
},
|
|
77
|
+
required: ['channel', 'timestamp', 'emoji'],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
}));
|
|
82
|
+
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
83
|
+
const { name, arguments: args } = req.params;
|
|
84
|
+
if (name === 'slack.reply') {
|
|
85
|
+
const { channel, text, thread_ts } = args;
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch('https://slack.com/api/chat.postMessage', {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
Authorization: `Bearer ${BOT_TOKEN}`,
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
channel,
|
|
95
|
+
text,
|
|
96
|
+
...(thread_ts ? { thread_ts } : {}),
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
const data = (await res.json());
|
|
100
|
+
if (!data.ok) {
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: 'text', text: `Slack error: ${data.error}` }],
|
|
103
|
+
isError: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return { content: [{ type: 'text', text: 'sent' }] };
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: 'text', text: `Failed: ${err.message}` }],
|
|
111
|
+
isError: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (name === 'slack.react') {
|
|
116
|
+
const { channel, timestamp, emoji } = args;
|
|
117
|
+
try {
|
|
118
|
+
const res = await fetch('https://slack.com/api/reactions.add', {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: {
|
|
121
|
+
'Content-Type': 'application/json',
|
|
122
|
+
Authorization: `Bearer ${BOT_TOKEN}`,
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify({ channel, timestamp, name: emoji }),
|
|
125
|
+
});
|
|
126
|
+
const data = (await res.json());
|
|
127
|
+
if (!data.ok && data.error !== 'already_reacted') {
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: 'text', text: `Slack error: ${data.error}` }],
|
|
130
|
+
isError: true,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return { content: [{ type: 'text', text: 'reacted' }] };
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: 'text', text: `Failed: ${err.message}` }],
|
|
138
|
+
isError: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
143
|
+
});
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Connect MCP to Claude Code
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
await mcp.connect(new StdioServerTransport());
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Socket Mode — connect to Slack and forward events
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Resolve bot user ID so we can filter self-messages
|
|
152
|
+
let botUserId = null;
|
|
153
|
+
async function getBotUserId() {
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch('https://slack.com/api/auth.test', {
|
|
156
|
+
headers: { Authorization: `Bearer ${BOT_TOKEN}` },
|
|
157
|
+
});
|
|
158
|
+
const data = (await res.json());
|
|
159
|
+
return data.ok ? data.user_id ?? null : null;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Socket Mode connection using raw WebSocket
|
|
166
|
+
// (avoids heavy @slack/bolt dependency in the MCP server)
|
|
167
|
+
async function connectSocketMode() {
|
|
168
|
+
// Get WebSocket URL
|
|
169
|
+
const res = await fetch('https://slack.com/api/apps.connections.open', {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: { Authorization: `Bearer ${APP_TOKEN}` },
|
|
172
|
+
});
|
|
173
|
+
const data = (await res.json());
|
|
174
|
+
if (!data.ok || !data.url) {
|
|
175
|
+
process.stderr.write(`slack-channel: Socket Mode connection failed: ${data.error}\n`);
|
|
176
|
+
// Retry after 10s
|
|
177
|
+
setTimeout(connectSocketMode, 10_000);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const ws = new WebSocket(data.url);
|
|
181
|
+
ws.onopen = () => {
|
|
182
|
+
process.stderr.write('slack-channel: Socket Mode connected\n');
|
|
183
|
+
};
|
|
184
|
+
ws.onmessage = async (event) => {
|
|
185
|
+
try {
|
|
186
|
+
const msg = JSON.parse(String(event.data));
|
|
187
|
+
// Acknowledge all envelope messages
|
|
188
|
+
if (msg.envelope_id) {
|
|
189
|
+
ws.send(JSON.stringify({ envelope_id: msg.envelope_id }));
|
|
190
|
+
}
|
|
191
|
+
// Only process event callbacks
|
|
192
|
+
if (msg.type !== 'events_api' || !msg.payload?.event)
|
|
193
|
+
return;
|
|
194
|
+
const evt = msg.payload.event;
|
|
195
|
+
// Skip bot's own messages
|
|
196
|
+
if (evt.user === botUserId)
|
|
197
|
+
return;
|
|
198
|
+
// Skip non-message events
|
|
199
|
+
if (evt.type !== 'app_mention' && evt.type !== 'message')
|
|
200
|
+
return;
|
|
201
|
+
// For message events in channels (not DMs), only process app_mention
|
|
202
|
+
// to avoid responding to every message in a channel
|
|
203
|
+
if (evt.type === 'message' && evt.channel && !evt.channel.startsWith('D'))
|
|
204
|
+
return;
|
|
205
|
+
// Sender gating
|
|
206
|
+
if (ALLOWED_USERS.size > 0 && evt.user && !ALLOWED_USERS.has(evt.user))
|
|
207
|
+
return;
|
|
208
|
+
const text = evt.text ?? '';
|
|
209
|
+
const channel = evt.channel ?? '';
|
|
210
|
+
const ts = evt.ts ?? '';
|
|
211
|
+
const threadTs = evt.thread_ts ?? ts;
|
|
212
|
+
const user = evt.user ?? 'unknown';
|
|
213
|
+
// Ack: add eyes reaction immediately so the user knows we saw it
|
|
214
|
+
if (channel && ts) {
|
|
215
|
+
fetch('https://slack.com/api/reactions.add', {
|
|
216
|
+
method: 'POST',
|
|
217
|
+
headers: {
|
|
218
|
+
'Content-Type': 'application/json',
|
|
219
|
+
Authorization: `Bearer ${BOT_TOKEN}`,
|
|
220
|
+
},
|
|
221
|
+
body: JSON.stringify({ channel, timestamp: ts, name: 'eyes' }),
|
|
222
|
+
}).catch(() => { }); // fire-and-forget, don't block
|
|
223
|
+
}
|
|
224
|
+
// Push to Claude session as a channel notification
|
|
225
|
+
await mcp.notification({
|
|
226
|
+
method: 'notifications/claude/channel',
|
|
227
|
+
params: {
|
|
228
|
+
content: text,
|
|
229
|
+
meta: {
|
|
230
|
+
user,
|
|
231
|
+
channel,
|
|
232
|
+
thread_ts: threadTs,
|
|
233
|
+
event_type: evt.type,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
process.stderr.write(`slack-channel: Event error: ${err.message}\n`);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
ws.onclose = () => {
|
|
243
|
+
process.stderr.write('slack-channel: Socket Mode disconnected, reconnecting...\n');
|
|
244
|
+
setTimeout(connectSocketMode, 5_000);
|
|
245
|
+
};
|
|
246
|
+
ws.onerror = (err) => {
|
|
247
|
+
process.stderr.write(`slack-channel: WebSocket error: ${err}\n`);
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// Start
|
|
251
|
+
botUserId = await getBotUserId();
|
|
252
|
+
process.stderr.write(`slack-channel: Bot user ID: ${botUserId}\n`);
|
|
253
|
+
connectSocketMode();
|
|
254
|
+
//# sourceMappingURL=slack-channel.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@integrity-labs/agt-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Augmented CLI — agent provisioning and management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,14 +12,15 @@
|
|
|
12
12
|
"directory": "apps/cli"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
-
"dist"
|
|
15
|
+
"dist",
|
|
16
|
+
"mcp"
|
|
16
17
|
],
|
|
17
18
|
"publishConfig": {
|
|
18
19
|
"registry": "https://registry.npmjs.org",
|
|
19
20
|
"access": "public"
|
|
20
21
|
},
|
|
21
22
|
"scripts": {
|
|
22
|
-
"build": "tsup",
|
|
23
|
+
"build": "tsup && mkdir -p mcp && cp ../../packages/mcp/dist/index.js mcp/index.js && cp ../../packages/mcp/dist/slack-channel.js mcp/slack-channel.js",
|
|
23
24
|
"dev": "tsx watch src/bin/agt.ts",
|
|
24
25
|
"test": "vitest run",
|
|
25
26
|
"typecheck": "tsc --noEmit",
|