@sensigo/realm-cli 0.1.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/README.md +109 -0
- package/dist/agent/agent-utils.d.ts +27 -0
- package/dist/agent/agent-utils.d.ts.map +1 -0
- package/dist/agent/agent-utils.js +79 -0
- package/dist/agent/agent-utils.js.map +1 -0
- package/dist/agent/anthropic-provider.d.ts +23 -0
- package/dist/agent/anthropic-provider.d.ts.map +1 -0
- package/dist/agent/anthropic-provider.js +245 -0
- package/dist/agent/anthropic-provider.js.map +1 -0
- package/dist/agent/gate/slack-gate-notifier.d.ts +80 -0
- package/dist/agent/gate/slack-gate-notifier.d.ts.map +1 -0
- package/dist/agent/gate/slack-gate-notifier.js +315 -0
- package/dist/agent/gate/slack-gate-notifier.js.map +1 -0
- package/dist/agent/gate/slack-gate-server.d.ts +30 -0
- package/dist/agent/gate/slack-gate-server.d.ts.map +1 -0
- package/dist/agent/gate/slack-gate-server.js +99 -0
- package/dist/agent/gate/slack-gate-server.js.map +1 -0
- package/dist/agent/gate/slack-socket-client.d.ts +20 -0
- package/dist/agent/gate/slack-socket-client.d.ts.map +1 -0
- package/dist/agent/gate/slack-socket-client.js +141 -0
- package/dist/agent/gate/slack-socket-client.js.map +1 -0
- package/dist/agent/gate-intent-interpreter.d.ts +29 -0
- package/dist/agent/gate-intent-interpreter.d.ts.map +1 -0
- package/dist/agent/gate-intent-interpreter.js +33 -0
- package/dist/agent/gate-intent-interpreter.js.map +1 -0
- package/dist/agent/index.d.ts +4 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +4 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/llm-provider.d.ts +46 -0
- package/dist/agent/llm-provider.d.ts.map +1 -0
- package/dist/agent/llm-provider.js +55 -0
- package/dist/agent/llm-provider.js.map +1 -0
- package/dist/agent/mcp/mcp-client.d.ts +18 -0
- package/dist/agent/mcp/mcp-client.d.ts.map +1 -0
- package/dist/agent/mcp/mcp-client.js +108 -0
- package/dist/agent/mcp/mcp-client.js.map +1 -0
- package/dist/agent/mcp/mcp-extensions.d.ts +40 -0
- package/dist/agent/mcp/mcp-extensions.d.ts.map +1 -0
- package/dist/agent/mcp/mcp-extensions.js +2 -0
- package/dist/agent/mcp/mcp-extensions.js.map +1 -0
- package/dist/agent/mcp-client.d.ts +18 -0
- package/dist/agent/mcp-client.d.ts.map +1 -0
- package/dist/agent/mcp-client.js +108 -0
- package/dist/agent/mcp-client.js.map +1 -0
- package/dist/agent/mcp-types.d.ts +40 -0
- package/dist/agent/mcp-types.d.ts.map +1 -0
- package/dist/agent/mcp-types.js +2 -0
- package/dist/agent/mcp-types.js.map +1 -0
- package/dist/agent/openai-provider.d.ts +30 -0
- package/dist/agent/openai-provider.d.ts.map +1 -0
- package/dist/agent/openai-provider.js +253 -0
- package/dist/agent/openai-provider.js.map +1 -0
- package/dist/agent/openai-reasoning-provider.d.ts +18 -0
- package/dist/agent/openai-reasoning-provider.d.ts.map +1 -0
- package/dist/agent/openai-reasoning-provider.js +76 -0
- package/dist/agent/openai-reasoning-provider.js.map +1 -0
- package/dist/agent/preflight.d.ts +36 -0
- package/dist/agent/preflight.d.ts.map +1 -0
- package/dist/agent/preflight.js +81 -0
- package/dist/agent/preflight.js.map +1 -0
- package/dist/agent/providers/agent-utils.d.ts +27 -0
- package/dist/agent/providers/agent-utils.d.ts.map +1 -0
- package/dist/agent/providers/agent-utils.js +79 -0
- package/dist/agent/providers/agent-utils.js.map +1 -0
- package/dist/agent/providers/anthropic-provider.d.ts +23 -0
- package/dist/agent/providers/anthropic-provider.d.ts.map +1 -0
- package/dist/agent/providers/anthropic-provider.js +257 -0
- package/dist/agent/providers/anthropic-provider.js.map +1 -0
- package/dist/agent/providers/llm-provider.d.ts +46 -0
- package/dist/agent/providers/llm-provider.d.ts.map +1 -0
- package/dist/agent/providers/llm-provider.js +56 -0
- package/dist/agent/providers/llm-provider.js.map +1 -0
- package/dist/agent/providers/openai-provider.d.ts +30 -0
- package/dist/agent/providers/openai-provider.d.ts.map +1 -0
- package/dist/agent/providers/openai-provider.js +253 -0
- package/dist/agent/providers/openai-provider.js.map +1 -0
- package/dist/agent/providers/openai-reasoning-provider.d.ts +19 -0
- package/dist/agent/providers/openai-reasoning-provider.d.ts.map +1 -0
- package/dist/agent/providers/openai-reasoning-provider.js +89 -0
- package/dist/agent/providers/openai-reasoning-provider.js.map +1 -0
- package/dist/agent/run-agent.d.ts +50 -0
- package/dist/agent/run-agent.d.ts.map +1 -0
- package/dist/agent/run-agent.js +327 -0
- package/dist/agent/run-agent.js.map +1 -0
- package/dist/agent/slack-gate-notifier.d.ts +80 -0
- package/dist/agent/slack-gate-notifier.d.ts.map +1 -0
- package/dist/agent/slack-gate-notifier.js +315 -0
- package/dist/agent/slack-gate-notifier.js.map +1 -0
- package/dist/agent/slack-gate-poller.d.ts +28 -0
- package/dist/agent/slack-gate-poller.d.ts.map +1 -0
- package/dist/agent/slack-gate-poller.js +66 -0
- package/dist/agent/slack-gate-poller.js.map +1 -0
- package/dist/agent/slack-gate-server.d.ts +30 -0
- package/dist/agent/slack-gate-server.d.ts.map +1 -0
- package/dist/agent/slack-gate-server.js +99 -0
- package/dist/agent/slack-gate-server.js.map +1 -0
- package/dist/agent/slack-socket-client.d.ts +20 -0
- package/dist/agent/slack-socket-client.d.ts.map +1 -0
- package/dist/agent/slack-socket-client.js +141 -0
- package/dist/agent/slack-socket-client.js.map +1 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +183 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/cleanup.d.ts +16 -0
- package/dist/commands/cleanup.d.ts.map +1 -0
- package/dist/commands/cleanup.js +79 -0
- package/dist/commands/cleanup.js.map +1 -0
- package/dist/commands/diff.d.ts +41 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +203 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +97 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/inspect.d.ts +14 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +224 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/list.d.ts +18 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +88 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/mcp.d.ts +8 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +22 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/migrate.d.ts +3 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +42 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/register.d.ts +6 -0
- package/dist/commands/register.d.ts.map +1 -0
- package/dist/commands/register.js +51 -0
- package/dist/commands/register.js.map +1 -0
- package/dist/commands/replay-format.d.ts +3 -0
- package/dist/commands/replay-format.d.ts.map +1 -0
- package/dist/commands/replay-format.js +10 -0
- package/dist/commands/replay-format.js.map +1 -0
- package/dist/commands/replay.d.ts +38 -0
- package/dist/commands/replay.d.ts.map +1 -0
- package/dist/commands/replay.js +173 -0
- package/dist/commands/replay.js.map +1 -0
- package/dist/commands/respond.d.ts +20 -0
- package/dist/commands/respond.d.ts.map +1 -0
- package/dist/commands/respond.js +49 -0
- package/dist/commands/respond.js.map +1 -0
- package/dist/commands/resume.d.ts +15 -0
- package/dist/commands/resume.d.ts.map +1 -0
- package/dist/commands/resume.js +63 -0
- package/dist/commands/resume.js.map +1 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +127 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/serve.d.ts +33 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +144 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/test.d.ts +12 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +58 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/validate.d.ts +3 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +35 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/watch.d.ts +18 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js +112 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/commands/webhook.d.ts +51 -0
- package/dist/commands/webhook.d.ts.map +1 -0
- package/dist/commands/webhook.js +227 -0
- package/dist/commands/webhook.js.map +1 -0
- package/dist/commands-registry.d.ts +7 -0
- package/dist/commands-registry.d.ts.map +1 -0
- package/dist/commands-registry.js +44 -0
- package/dist/commands-registry.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/store/replay-store.d.ts +29 -0
- package/dist/store/replay-store.d.ts.map +1 -0
- package/dist/store/replay-store.js +31 -0
- package/dist/store/replay-store.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// slack-gate-notifier.ts — Slack gate notification logic.
|
|
2
|
+
// Owns all Slack-specific gate notification: formatting, posting, reminder timers,
|
|
3
|
+
// and bidirectional resolution via Events API or Socket Mode.
|
|
4
|
+
// Internal to the CLI package — not exported from the package's public index.
|
|
5
|
+
import { submitHumanResponse, } from '@sensigo/realm';
|
|
6
|
+
import { startSlackGateServer } from './slack-gate-server.js';
|
|
7
|
+
import { connectSocketMode } from './slack-socket-client.js';
|
|
8
|
+
/**
|
|
9
|
+
* Formats a gate preview object as human-readable Slack mrkdwn text.
|
|
10
|
+
* Uses `headline` and `message` fields when present; falls back to indented JSON.
|
|
11
|
+
* Exported for testing.
|
|
12
|
+
*/
|
|
13
|
+
export function formatGatePreviewForSlack(preview) {
|
|
14
|
+
const headline = typeof preview['headline'] === 'string' ? preview['headline'] : undefined;
|
|
15
|
+
const message = typeof preview['message'] === 'string' ? preview['message'] : undefined;
|
|
16
|
+
if (headline !== undefined || message !== undefined) {
|
|
17
|
+
const parts = [];
|
|
18
|
+
if (headline !== undefined)
|
|
19
|
+
parts.push(`*${headline}*`);
|
|
20
|
+
if (message !== undefined)
|
|
21
|
+
parts.push(message);
|
|
22
|
+
return parts.join('\n\n');
|
|
23
|
+
}
|
|
24
|
+
if (Object.keys(preview).length === 0) {
|
|
25
|
+
return '_(no preview)_';
|
|
26
|
+
}
|
|
27
|
+
return '```\n' + JSON.stringify(preview, null, 2) + '\n```';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* POSTs a gate-waiting notification to a Slack Incoming Webhook.
|
|
31
|
+
* Failure is warned but does not abort the workflow run.
|
|
32
|
+
* Exported for testing.
|
|
33
|
+
*/
|
|
34
|
+
export async function postGateNotificationToSlack(webhookUrl, gate, runId) {
|
|
35
|
+
const ownerLine = gate.owner !== undefined ? `\n*Owner:* ${gate.owner}` : '';
|
|
36
|
+
const previewText = gate.resolved_message ?? formatGatePreviewForSlack(gate.preview);
|
|
37
|
+
const gateId = gate.gate_id;
|
|
38
|
+
const cmdLines = gate.choices
|
|
39
|
+
.map((c) => `realm run respond ${runId} --gate ${gateId} --choice ${c}`)
|
|
40
|
+
.join('\n');
|
|
41
|
+
const blockText = `*Gate:* \`${gate.step_name}\`${ownerLine}\n\n${previewText}\n\n---\n` +
|
|
42
|
+
`*Gate requires a terminal response — open a terminal and run:*\n\`\`\`${cmdLines}\`\`\``;
|
|
43
|
+
const body = {
|
|
44
|
+
text: '⏸ Workflow gate waiting for approval',
|
|
45
|
+
blocks: [
|
|
46
|
+
{
|
|
47
|
+
type: 'section',
|
|
48
|
+
text: {
|
|
49
|
+
type: 'mrkdwn',
|
|
50
|
+
text: blockText,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
await fetch(webhookUrl, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: { 'Content-Type': 'application/json' },
|
|
59
|
+
body: JSON.stringify(body),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
console.warn(` ⚠ Slack notification failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Posts a gate notification to Slack via chat.postMessage (bot token path).
|
|
68
|
+
* Returns the message ts for use as a thread anchor, or undefined on failure.
|
|
69
|
+
* Exported for testing.
|
|
70
|
+
*/
|
|
71
|
+
export async function postGateViaApi(botToken, channelId, gate, runId) {
|
|
72
|
+
const ownerLine = gate.owner !== undefined ? `\n*Owner:* ${gate.owner}` : '';
|
|
73
|
+
const previewText = gate.resolved_message ?? formatGatePreviewForSlack(gate.preview);
|
|
74
|
+
const choiceList = gate.choices.map((c) => `\`${c}\``).join(' or ');
|
|
75
|
+
const gateId = gate.gate_id;
|
|
76
|
+
const cmdLines = gate.choices
|
|
77
|
+
.map((c) => `realm run respond ${runId} --gate ${gateId} --choice ${c}`)
|
|
78
|
+
.join('\n');
|
|
79
|
+
const blockText = `*Gate:* \`${gate.step_name}\`${ownerLine}\n\n${previewText}\n\n---\n` +
|
|
80
|
+
`*Reply in this thread with ${choiceList} to resolve, or run:*\n\`\`\`${cmdLines}\`\`\``;
|
|
81
|
+
try {
|
|
82
|
+
const response = await fetch('https://slack.com/api/chat.postMessage', {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: {
|
|
85
|
+
'Content-Type': 'application/json',
|
|
86
|
+
Authorization: `Bearer ${botToken}`,
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
channel: channelId,
|
|
90
|
+
text: '⏸ Workflow gate waiting for approval',
|
|
91
|
+
blocks: [{ type: 'section', text: { type: 'mrkdwn', text: blockText } }],
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
const data = (await response.json());
|
|
95
|
+
return data.ok ? data.ts : undefined;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
console.warn(`Gate API notification failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Posts a reply to an existing Slack thread via chat.postMessage.
|
|
104
|
+
* Failures are warned but not thrown — best-effort.
|
|
105
|
+
* Exported for testing.
|
|
106
|
+
*/
|
|
107
|
+
export async function postSlackReply(botToken, channelId, threadTs, text) {
|
|
108
|
+
try {
|
|
109
|
+
await fetch('https://slack.com/api/chat.postMessage', {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: {
|
|
112
|
+
'Content-Type': 'application/json',
|
|
113
|
+
Authorization: `Bearer ${botToken}`,
|
|
114
|
+
},
|
|
115
|
+
body: JSON.stringify({ channel: channelId, thread_ts: threadTs, text }),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
console.warn(`Slack reply failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Starts reminder and escalation timers for an open gate.
|
|
124
|
+
* Returns a cleanup function that clears both timers — must be called when the gate resolves.
|
|
125
|
+
* Exported for testing.
|
|
126
|
+
*/
|
|
127
|
+
export function startGateReminderTimers(botToken, channelId, threadTs, gate, reminderIntervalMs, escalationThresholdMs) {
|
|
128
|
+
const reminderTimer = setTimeout(() => {
|
|
129
|
+
postSlackReply(botToken, channelId, threadTs, `⏰ Reminder: gate \`${gate.step_name}\` is still waiting for a response.`).catch((err) => {
|
|
130
|
+
console.warn(`Reminder post failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
131
|
+
});
|
|
132
|
+
}, reminderIntervalMs);
|
|
133
|
+
const escalationTimer = setTimeout(() => {
|
|
134
|
+
const elapsedMin = Math.round((Date.now() - new Date(gate.opened_at).getTime()) / 60_000);
|
|
135
|
+
const mention = gate.owner !== undefined ? `${gate.owner} — ` : '';
|
|
136
|
+
const text = `${mention}gate \`${gate.step_name}\` has been open for ${elapsedMin} minutes. Please respond.`;
|
|
137
|
+
postSlackReply(botToken, channelId, threadTs, text).catch((err) => {
|
|
138
|
+
console.warn(`Escalation post failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
139
|
+
});
|
|
140
|
+
}, escalationThresholdMs);
|
|
141
|
+
return () => {
|
|
142
|
+
clearTimeout(reminderTimer);
|
|
143
|
+
clearTimeout(escalationTimer);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Handles a gate using Slack bidirectional resolution:
|
|
148
|
+
* - Events API (when signing secret is present) or Socket Mode (when app token is present)
|
|
149
|
+
* - LLM intent interpretation of Slack replies
|
|
150
|
+
* - Reminder and escalation timers
|
|
151
|
+
* - Falls back to store polling for terminal-command gate resolution
|
|
152
|
+
* Exported for testing.
|
|
153
|
+
*/
|
|
154
|
+
export async function handleBidirectionalGate(params) {
|
|
155
|
+
const { gate, runId, definition, store, provider: _provider, slackBotToken, slackChannelId, gateThreadTs, slackSigningSecret, slackEventsPort, slackAppToken, gateReminderIntervalMs, gateEscalationThresholdMs, pollIntervalMs, } = params;
|
|
156
|
+
let clarificationCount = 0;
|
|
157
|
+
const abortController = new AbortController();
|
|
158
|
+
const processCandidate = async (text) => {
|
|
159
|
+
if (abortController.signal.aborted)
|
|
160
|
+
return;
|
|
161
|
+
// Gate choices must be exact (case-insensitive). No LLM interpretation — gate responses
|
|
162
|
+
// are irreversible writes and must reflect unambiguous intent.
|
|
163
|
+
const normalised = text.trim().toLowerCase();
|
|
164
|
+
const exactMatch = gate.choices.find((c) => c.toLowerCase() === normalised);
|
|
165
|
+
if (exactMatch !== undefined) {
|
|
166
|
+
try {
|
|
167
|
+
await submitHumanResponse(store, definition, {
|
|
168
|
+
runId,
|
|
169
|
+
gateId: gate.gate_id,
|
|
170
|
+
choice: exactMatch,
|
|
171
|
+
});
|
|
172
|
+
if (gateThreadTs !== undefined) {
|
|
173
|
+
const confirmationText = gate.resolution_messages?.[exactMatch] ??
|
|
174
|
+
`✅ Gate resolved: \`${exactMatch}\` — run continuing.`;
|
|
175
|
+
await postSlackReply(slackBotToken, slackChannelId, gateThreadTs, confirmationText);
|
|
176
|
+
}
|
|
177
|
+
abortController.abort();
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
console.warn(`Gate response submission failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else if (clarificationCount < 2 && gateThreadTs !== undefined) {
|
|
184
|
+
clarificationCount++;
|
|
185
|
+
await postSlackReply(slackBotToken, slackChannelId, gateThreadTs, `Please reply with one of: ${gate.choices.join(', ')}`);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
// Set up candidate intake — Socket Mode (preferred), Events API, or none.
|
|
189
|
+
// Dedup set prevents duplicate onEvent calls. Socket Mode (preferred) and Events API both
|
|
190
|
+
// have at-least-once delivery — the same event can arrive more than once.
|
|
191
|
+
const seenEventIds = new Set();
|
|
192
|
+
let serverHandle;
|
|
193
|
+
if (slackAppToken !== undefined && gateThreadTs !== undefined) {
|
|
194
|
+
// Mode 2 — Socket Mode (WebSocket push, no public URL required)
|
|
195
|
+
serverHandle = connectSocketMode({
|
|
196
|
+
appToken: slackAppToken,
|
|
197
|
+
threadTs: gateThreadTs,
|
|
198
|
+
onEvent: (event) => {
|
|
199
|
+
if (seenEventIds.has(event.event_id))
|
|
200
|
+
return;
|
|
201
|
+
seenEventIds.add(event.event_id);
|
|
202
|
+
processCandidate(event.text).catch((err) => {
|
|
203
|
+
console.warn(`Candidate processing failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
signal: abortController.signal,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
else if (slackSigningSecret !== undefined && gateThreadTs !== undefined) {
|
|
210
|
+
// Mode 3 — Events API (HTTP push, requires public URL)
|
|
211
|
+
serverHandle = startSlackGateServer({
|
|
212
|
+
port: slackEventsPort,
|
|
213
|
+
signingSecret: slackSigningSecret,
|
|
214
|
+
onEvent: (event) => {
|
|
215
|
+
if (seenEventIds.has(event.event_id))
|
|
216
|
+
return;
|
|
217
|
+
seenEventIds.add(event.event_id);
|
|
218
|
+
if (event.thread_ts !== gateThreadTs)
|
|
219
|
+
return;
|
|
220
|
+
processCandidate(event.text).catch((err) => {
|
|
221
|
+
console.warn(`Candidate processing failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else if (gateThreadTs !== undefined) {
|
|
227
|
+
// Gate notification posted but no listener configured.
|
|
228
|
+
console.log(' ℹ Gate notification posted. Set SLACK_APP_TOKEN (Socket Mode) or SLACK_SIGNING_SECRET (Events API) to resolve via Slack. Using terminal command fallback.');
|
|
229
|
+
}
|
|
230
|
+
// Notify when neither Events nor polling can be used — no thread anchor available.
|
|
231
|
+
if (gateThreadTs === undefined) {
|
|
232
|
+
console.log(' ℹ Bidirectional Slack resolution unavailable (no thread anchor). Use terminal command shown above.');
|
|
233
|
+
}
|
|
234
|
+
// Start reminder/escalation timers if we have a thread anchor.
|
|
235
|
+
const clearTimers = gateThreadTs !== undefined
|
|
236
|
+
? startGateReminderTimers(slackBotToken, slackChannelId, gateThreadTs, gate, gateReminderIntervalMs, gateEscalationThresholdMs)
|
|
237
|
+
: () => { };
|
|
238
|
+
try {
|
|
239
|
+
// Poll the store as the unified done-detector — resolves when candidate processor
|
|
240
|
+
// calls submitHumanResponse OR when the terminal command is used.
|
|
241
|
+
await pollUntilGateResolved(store, runId, gate.gate_id, pollIntervalMs, abortController.signal);
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
abortController.abort();
|
|
245
|
+
clearTimers();
|
|
246
|
+
serverHandle?.close();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** Polls the store until the gate is resolved or the run reaches a terminal state. */
|
|
250
|
+
async function pollUntilGateResolved(store, runId, gateId, intervalMs, signal) {
|
|
251
|
+
console.log(' Waiting for approval...');
|
|
252
|
+
for (;;) {
|
|
253
|
+
if (signal?.aborted)
|
|
254
|
+
break;
|
|
255
|
+
await new Promise((resolve) => {
|
|
256
|
+
const timer = setTimeout(resolve, intervalMs);
|
|
257
|
+
if (signal !== undefined) {
|
|
258
|
+
signal.addEventListener('abort', () => {
|
|
259
|
+
clearTimeout(timer);
|
|
260
|
+
resolve();
|
|
261
|
+
}, { once: true });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
if (signal?.aborted)
|
|
265
|
+
break;
|
|
266
|
+
const run = await store.get(runId);
|
|
267
|
+
if (run.terminal_state)
|
|
268
|
+
break;
|
|
269
|
+
if (run.pending_gate === undefined || run.pending_gate.gate_id !== gateId)
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Creates a gate handler that notifies via Slack and waits for resolution.
|
|
275
|
+
* Returns a function typed (runId, gate) => Promise<void>.
|
|
276
|
+
*/
|
|
277
|
+
export function createSlackGateHandler(config) {
|
|
278
|
+
return async (runId, gate) => {
|
|
279
|
+
// Bidirectional path (bot token + channel).
|
|
280
|
+
if (config.botToken !== undefined && config.channelId !== undefined) {
|
|
281
|
+
const gateThreadTs = await postGateViaApi(config.botToken, config.channelId, gate, runId);
|
|
282
|
+
await handleBidirectionalGate({
|
|
283
|
+
gate,
|
|
284
|
+
runId,
|
|
285
|
+
definition: config.definition,
|
|
286
|
+
store: config.store,
|
|
287
|
+
provider: config.provider,
|
|
288
|
+
slackBotToken: config.botToken,
|
|
289
|
+
slackChannelId: config.channelId,
|
|
290
|
+
gateThreadTs,
|
|
291
|
+
...(config.signingSecret !== undefined ? { slackSigningSecret: config.signingSecret } : {}),
|
|
292
|
+
slackEventsPort: config.eventsPort ?? 3100,
|
|
293
|
+
...(config.appToken !== undefined ? { slackAppToken: config.appToken } : {}),
|
|
294
|
+
gateReminderIntervalMs: config.reminderIntervalMs ?? 600_000,
|
|
295
|
+
gateEscalationThresholdMs: config.escalationThresholdMs ?? 1_800_000,
|
|
296
|
+
pollIntervalMs: config.pollIntervalMs ?? 3000,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
else if (config.webhookUrl !== undefined) {
|
|
300
|
+
// One-way webhook + inline poll loop (avoids circular dependency with run-agent.ts).
|
|
301
|
+
await postGateNotificationToSlack(config.webhookUrl, gate, runId);
|
|
302
|
+
console.log(' Waiting for approval...');
|
|
303
|
+
const intervalMs = config.pollIntervalMs ?? 3000;
|
|
304
|
+
for (;;) {
|
|
305
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
306
|
+
const run = await config.store.get(runId);
|
|
307
|
+
if (run.terminal_state)
|
|
308
|
+
break;
|
|
309
|
+
if (run.pending_gate === undefined || run.pending_gate.gate_id !== gate.gate_id)
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
//# sourceMappingURL=slack-gate-notifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-gate-notifier.js","sourceRoot":"","sources":["../../src/agent/slack-gate-notifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface SlackPollConfig {
|
|
2
|
+
/** SLACK_BOT_TOKEN — used in Authorization header. */
|
|
3
|
+
botToken: string;
|
|
4
|
+
/** SLACK_CHANNEL_ID — channel containing the gate notification thread. */
|
|
5
|
+
channelId: string;
|
|
6
|
+
/** ts of the gate notification message — anchor for conversations.replies. */
|
|
7
|
+
threadTs: string;
|
|
8
|
+
/** Only process messages sent after this time. Set to gate opened_at. */
|
|
9
|
+
gateOpenedAt: Date;
|
|
10
|
+
/** Polling interval in milliseconds. Default: 10000. */
|
|
11
|
+
intervalMs: number;
|
|
12
|
+
/** Called with the text of each new human message. */
|
|
13
|
+
onCandidate: (text: string) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Called once after the first successful poll that finds no human replies.
|
|
16
|
+
* Use to surface a hint reminding the user to reply in the thread, not the main channel.
|
|
17
|
+
*/
|
|
18
|
+
onHint?: (message: string) => void;
|
|
19
|
+
/** When aborted, polling stops cleanly after the current interval. */
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Begins polling a Slack thread for new human replies.
|
|
24
|
+
* Filters out bot messages, messages before `gateOpenedAt`, and duplicate messages.
|
|
25
|
+
* Calls `onCandidate` at most once per unique message ts.
|
|
26
|
+
*/
|
|
27
|
+
export declare function pollSlackThread(config: SlackPollConfig): void;
|
|
28
|
+
//# sourceMappingURL=slack-gate-poller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-gate-poller.d.ts","sourceRoot":"","sources":["../../src/agent/slack-gate-poller.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,eAAe;IAC9B,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,YAAY,EAAE,IAAI,CAAC;IACnB,wDAAwD;IACxD,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,sEAAsE;IACtE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAmE7D"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// slack-gate-poller.ts — Polls a Slack thread for human replies when Events API is not configured.
|
|
2
|
+
// Used as a local CLI fallback when SLACK_BOT_TOKEN and SLACK_CHANNEL_ID are set
|
|
3
|
+
// but no Events API endpoint (SLACK_SIGNING_SECRET) is available.
|
|
4
|
+
/**
|
|
5
|
+
* Begins polling a Slack thread for new human replies.
|
|
6
|
+
* Filters out bot messages, messages before `gateOpenedAt`, and duplicate messages.
|
|
7
|
+
* Calls `onCandidate` at most once per unique message ts.
|
|
8
|
+
*/
|
|
9
|
+
export function pollSlackThread(config) {
|
|
10
|
+
const { botToken, channelId, threadTs, gateOpenedAt, intervalMs, onCandidate, onHint, signal } = config;
|
|
11
|
+
const processed = new Set();
|
|
12
|
+
// Convert gateOpenedAt to a Slack-style float timestamp string for comparison.
|
|
13
|
+
const gateOpenedSec = gateOpenedAt.getTime() / 1000;
|
|
14
|
+
let hintFired = false;
|
|
15
|
+
const poll = async () => {
|
|
16
|
+
if (signal?.aborted)
|
|
17
|
+
return;
|
|
18
|
+
try {
|
|
19
|
+
const url = `https://slack.com/api/conversations.replies` +
|
|
20
|
+
`?channel=${encodeURIComponent(channelId)}&ts=${encodeURIComponent(threadTs)}`;
|
|
21
|
+
const response = await fetch(url, {
|
|
22
|
+
headers: { Authorization: `Bearer ${botToken}` },
|
|
23
|
+
});
|
|
24
|
+
const data = (await response.json());
|
|
25
|
+
if (data.ok && Array.isArray(data.messages)) {
|
|
26
|
+
let foundHumanReply = false;
|
|
27
|
+
for (const msg of data.messages) {
|
|
28
|
+
const msgTs = msg['ts'];
|
|
29
|
+
if (!msgTs)
|
|
30
|
+
continue;
|
|
31
|
+
// Skip messages sent before or at gate opened time.
|
|
32
|
+
if (parseFloat(msgTs) <= gateOpenedSec)
|
|
33
|
+
continue;
|
|
34
|
+
// Filter bot messages.
|
|
35
|
+
if (msg['subtype'] === 'bot_message' || msg['bot_id'] !== undefined)
|
|
36
|
+
continue;
|
|
37
|
+
// Deduplicate by ts.
|
|
38
|
+
if (processed.has(msgTs))
|
|
39
|
+
continue;
|
|
40
|
+
processed.add(msgTs);
|
|
41
|
+
foundHumanReply = true;
|
|
42
|
+
const text = msg['text'];
|
|
43
|
+
if (text)
|
|
44
|
+
onCandidate(text);
|
|
45
|
+
}
|
|
46
|
+
// Fire the hint once after the first successful poll with no human replies.
|
|
47
|
+
if (!foundHumanReply && !hintFired && onHint !== undefined) {
|
|
48
|
+
hintFired = true;
|
|
49
|
+
onHint('Waiting for a thread reply. To resolve this gate, click \u201cReply\u201d on the gate message in Slack — do not post in the main channel.');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Polling failures are silent — gate remains open until a valid reply arrives.
|
|
55
|
+
}
|
|
56
|
+
if (!signal?.aborted) {
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
void poll();
|
|
59
|
+
}, intervalMs);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
void poll();
|
|
64
|
+
}, intervalMs);
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=slack-gate-poller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-gate-poller.js","sourceRoot":"","sources":["../../src/agent/slack-gate-poller.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,iFAAiF;AACjF,kEAAkE;AAwBlE;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAuB;IACrD,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,GAC5F,MAAM,CAAC;IAET,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,+EAA+E;IAC/E,MAAM,aAAa,GAAG,YAAY,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC;IACpD,IAAI,SAAS,GAAG,KAAK,CAAC;IAEtB,MAAM,IAAI,GAAG,KAAK,IAAmB,EAAE;QACrC,IAAI,MAAM,EAAE,OAAO;YAAE,OAAO;QAE5B,IAAI,CAAC;YACH,MAAM,GAAG,GACP,6CAA6C;gBAC7C,YAAY,kBAAkB,CAAC,SAAS,CAAC,OAAO,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;YACjF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAChC,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,QAAQ,EAAE,EAAE;aACjD,CAAC,CAAC;YACH,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAGlC,CAAC;YAEF,IAAI,IAAI,CAAC,EAAE,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC5C,IAAI,eAAe,GAAG,KAAK,CAAC;gBAC5B,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;oBAChC,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAuB,CAAC;oBAC9C,IAAI,CAAC,KAAK;wBAAE,SAAS;oBAErB,oDAAoD;oBACpD,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,aAAa;wBAAE,SAAS;oBAEjD,uBAAuB;oBACvB,IAAI,GAAG,CAAC,SAAS,CAAC,KAAK,aAAa,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,SAAS;wBAAE,SAAS;oBAE9E,qBAAqB;oBACrB,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;wBAAE,SAAS;oBACnC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;oBAErB,eAAe,GAAG,IAAI,CAAC;oBACvB,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAuB,CAAC;oBAC/C,IAAI,IAAI;wBAAE,WAAW,CAAC,IAAI,CAAC,CAAC;gBAC9B,CAAC;gBAED,4EAA4E;gBAC5E,IAAI,CAAC,eAAe,IAAI,CAAC,SAAS,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;oBAC3D,SAAS,GAAG,IAAI,CAAC;oBACjB,MAAM,CACJ,2IAA2I,CAC5I,CAAC;gBACJ,CAAC;YACH,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,+EAA+E;QACjF,CAAC;QAED,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;YACrB,UAAU,CAAC,GAAG,EAAE;gBACd,KAAK,IAAI,EAAE,CAAC;YACd,CAAC,EAAE,UAAU,CAAC,CAAC;QACjB,CAAC;IACH,CAAC,CAAC;IAEF,UAAU,CAAC,GAAG,EAAE;QACd,KAAK,IAAI,EAAE,CAAC;IACd,CAAC,EAAE,UAAU,CAAC,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** A verified, filtered Slack message event from the gate thread. */
|
|
2
|
+
export interface SlackGateEvent {
|
|
3
|
+
/** Dedupe key from the Slack event envelope. */
|
|
4
|
+
event_id: string;
|
|
5
|
+
/** Thread timestamp — must match the gate notification message ts. */
|
|
6
|
+
thread_ts: string;
|
|
7
|
+
/** Slack user ID of the message author. */
|
|
8
|
+
user: string;
|
|
9
|
+
/** Raw message text to be interpreted. */
|
|
10
|
+
text: string;
|
|
11
|
+
/** Slack event timestamp (format: '1234567890.123456'). */
|
|
12
|
+
ts: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SlackGateServerOptions {
|
|
15
|
+
/** Local port to listen on. Default: 3100. Configurable via SLACK_EVENTS_PORT. */
|
|
16
|
+
port: number;
|
|
17
|
+
/** Slack signing secret for HMAC-SHA256 verification. Never logged. */
|
|
18
|
+
signingSecret: string;
|
|
19
|
+
/** Called for each valid, non-bot message event that passes verification. */
|
|
20
|
+
onEvent: (event: SlackGateEvent) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Starts a local HTTP server that receives Slack Events API callbacks.
|
|
24
|
+
* Verifies request signatures before processing any payload.
|
|
25
|
+
* Returns a handle to close the server.
|
|
26
|
+
*/
|
|
27
|
+
export declare function startSlackGateServer(options: SlackGateServerOptions): {
|
|
28
|
+
close(): void;
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=slack-gate-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-gate-server.d.ts","sourceRoot":"","sources":["../../src/agent/slack-gate-server.ts"],"names":[],"mappings":"AAKA,qEAAqE;AACrE,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,sBAAsB;IACrC,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,aAAa,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;CAC1C;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,sBAAsB,GAAG;IAAE,KAAK,IAAI,IAAI,CAAA;CAAE,CAoGvF"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// slack-gate-server.ts — Minimal HTTP server for Slack Events API callbacks.
|
|
2
|
+
// Handles POST /slack/events with HMAC-SHA256 signature verification.
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
5
|
+
/**
|
|
6
|
+
* Starts a local HTTP server that receives Slack Events API callbacks.
|
|
7
|
+
* Verifies request signatures before processing any payload.
|
|
8
|
+
* Returns a handle to close the server.
|
|
9
|
+
*/
|
|
10
|
+
export function startSlackGateServer(options) {
|
|
11
|
+
const { port, signingSecret, onEvent } = options;
|
|
12
|
+
const server = createServer((req, res) => {
|
|
13
|
+
if (req.method !== 'POST' || req.url !== '/slack/events') {
|
|
14
|
+
res.writeHead(404);
|
|
15
|
+
res.end();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const chunks = [];
|
|
19
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
20
|
+
req.on('end', () => {
|
|
21
|
+
const rawBody = Buffer.concat(chunks).toString('utf8');
|
|
22
|
+
const timestamp = req.headers['x-slack-request-timestamp'];
|
|
23
|
+
const signature = req.headers['x-slack-signature'];
|
|
24
|
+
if (typeof timestamp !== 'string' || typeof signature !== 'string') {
|
|
25
|
+
res.writeHead(403);
|
|
26
|
+
res.end('Missing Slack signature headers');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Reject stale requests before performing HMAC — prevents replay attacks.
|
|
30
|
+
const requestTime = parseInt(timestamp, 10);
|
|
31
|
+
if (isNaN(requestTime)) {
|
|
32
|
+
res.writeHead(400);
|
|
33
|
+
res.end('Invalid timestamp');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
37
|
+
if (Math.abs(nowSec - requestTime) > 300) {
|
|
38
|
+
res.writeHead(403);
|
|
39
|
+
res.end('Stale request');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Verify HMAC-SHA256 signature using timing-safe comparison.
|
|
43
|
+
const baseString = `v0:${timestamp}:${rawBody}`;
|
|
44
|
+
const expected = 'v0=' + createHmac('sha256', signingSecret).update(baseString).digest('hex');
|
|
45
|
+
const expectedBuf = Buffer.from(expected, 'utf8');
|
|
46
|
+
const receivedBuf = Buffer.from(signature, 'utf8');
|
|
47
|
+
const signaturesMatch = expectedBuf.length === receivedBuf.length && timingSafeEqual(expectedBuf, receivedBuf);
|
|
48
|
+
if (!signaturesMatch) {
|
|
49
|
+
res.writeHead(403);
|
|
50
|
+
res.end('Invalid signature');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Parse the verified payload.
|
|
54
|
+
let payload;
|
|
55
|
+
try {
|
|
56
|
+
payload = JSON.parse(rawBody);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
res.writeHead(400);
|
|
60
|
+
res.end('Invalid JSON');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Handle Slack URL verification challenge.
|
|
64
|
+
if (payload['type'] === 'url_verification') {
|
|
65
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
66
|
+
res.end(JSON.stringify({ challenge: payload['challenge'] }));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Acknowledge immediately — Slack requires a response within 3 seconds.
|
|
70
|
+
res.writeHead(200);
|
|
71
|
+
res.end();
|
|
72
|
+
if (payload['type'] !== 'event_callback')
|
|
73
|
+
return;
|
|
74
|
+
const event = payload['event'];
|
|
75
|
+
if (event === undefined)
|
|
76
|
+
return;
|
|
77
|
+
if (event['type'] !== 'message')
|
|
78
|
+
return;
|
|
79
|
+
// Filter bot messages — only route human replies.
|
|
80
|
+
if (event['subtype'] === 'bot_message' || event['bot_id'] !== undefined)
|
|
81
|
+
return;
|
|
82
|
+
const threadTs = event['thread_ts'];
|
|
83
|
+
const user = event['user'];
|
|
84
|
+
const text = event['text'];
|
|
85
|
+
const ts = event['ts'];
|
|
86
|
+
const eventId = payload['event_id'];
|
|
87
|
+
if (!threadTs || !user || !text || !ts || !eventId)
|
|
88
|
+
return;
|
|
89
|
+
onEvent({ event_id: eventId, thread_ts: threadTs, user, text, ts });
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
server.listen(port);
|
|
93
|
+
return {
|
|
94
|
+
close() {
|
|
95
|
+
server.close();
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=slack-gate-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-gate-server.js","sourceRoot":"","sources":["../../src/agent/slack-gate-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SlackGateEvent } from './slack-gate-server.js';
|
|
2
|
+
export interface SlackSocketConfig {
|
|
3
|
+
/** SLACK_APP_TOKEN — App-level token (starts with xapp-). */
|
|
4
|
+
appToken: string;
|
|
5
|
+
/** Gate thread timestamp — only events in this thread are forwarded to onEvent. */
|
|
6
|
+
threadTs: string;
|
|
7
|
+
/** Called for each valid human message in the gate thread. */
|
|
8
|
+
onEvent: (event: SlackGateEvent) => void;
|
|
9
|
+
/** When aborted, the connection is closed and no reconnects are attempted. */
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Opens a Slack Socket Mode WebSocket connection and begins delivering gate thread events.
|
|
14
|
+
* Handles reconnection transparently on drops or Slack-requested refreshes.
|
|
15
|
+
* Returns a handle to close the connection immediately.
|
|
16
|
+
*/
|
|
17
|
+
export declare function connectSocketMode(config: SlackSocketConfig): {
|
|
18
|
+
close(): void;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=slack-socket-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-socket-client.d.ts","sourceRoot":"","sources":["../../src/agent/slack-socket-client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IAChC,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IACzC,8EAA8E;IAC9E,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG;IAAE,KAAK,IAAI,IAAI,CAAA;CAAE,CAoJ9E"}
|