tide-commander 1.62.0 → 1.63.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/dist/assets/{BossLogsModal-Cc26KWtF.js → BossLogsModal-BpNtzCTX.js} +1 -1
- package/dist/assets/{BossSpawnModal-79zixDGu.js → BossSpawnModal-ByiEkNZl.js} +1 -1
- package/dist/assets/{ControlsModal-sIxaSPTd.js → ControlsModal-BgeLGCGT.js} +1 -1
- package/dist/assets/{DockerLogsModal-RAIqQDHy.js → DockerLogsModal-CaK9YJT4.js} +1 -1
- package/dist/assets/{EmbeddedEditor-SYtH65mY.js → EmbeddedEditor-DcGJCiuP.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-DK2GlEv1.js → GmailOAuthSetup-CYIBkOXn.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-CprVeTSW.js → GoogleOAuthSetup-CY_EF39t.js} +1 -1
- package/dist/assets/{IframeModal-pzXWPx2i.js → IframeModal-DDFIBCq1.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BzPNGPs-.js → IntegrationsPanel-CssC1pup.js} +2 -2
- package/dist/assets/{LogViewerModal-oqzbJ5iy.js → LogViewerModal-C_f0Z-2y.js} +1 -1
- package/dist/assets/{MonitoringModal-DadiFtmu.js → MonitoringModal-CkOkdUvI.js} +1 -1
- package/dist/assets/{PM2LogsModal-C4hQRREX.js → PM2LogsModal-BtqM8TkL.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-fHOJ9ZwM.js → RestoreArchivedAreaModal-CIqWIO9m.js} +1 -1
- package/dist/assets/{Scene2DCanvas-BeHXBqTf.js → Scene2DCanvas-CJ_MZU-J.js} +1 -1
- package/dist/assets/{SceneManager-B6E_cAYg.js → SceneManager-DyX0JqRQ.js} +1 -1
- package/dist/assets/{SkillsPanel-B3-ni6t4.js → SkillsPanel-DR7WNV5l.js} +1 -1
- package/dist/assets/{SpawnModal-00Lv0ZQy.js → SpawnModal-BCJDoSQg.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-DVgwAkbS.js → SubordinateAssignmentModal-DAlLWBtv.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-GUPU3sup.js → TriggerManagerPanel-ChBctjk3.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-CP26YA9V.js → WorkflowEditorPanel-Bk-QD7-7.js} +1 -1
- package/dist/assets/{index-alq8vlxv.js → index-COaYNxnC.js} +1 -1
- package/dist/assets/{index-DwJ7OcGh.js → index-CVIkvsfJ.js} +1 -1
- package/dist/assets/{index-B9SzB9ov.js → index-DcxPBYWc.js} +1 -1
- package/dist/assets/{index-BS2yVyno.js → index-DoOLLVEG.js} +2 -2
- package/dist/assets/{index-DLkUpBdu.js → index-DwTs55lG.js} +1 -1
- package/dist/assets/{index-C7PPAB37.js → index-Dwt45QE5.js} +1 -1
- package/dist/assets/{index-D8tCoR93.js → index-M-tYhxQp.js} +3 -3
- package/dist/assets/{index-DKziSiqq.js → index-vx9STDIr.js} +1 -1
- package/dist/assets/{main-DxFgIntP.js → main-5l14ueeE.js} +4 -4
- package/dist/assets/{web-BGvcteqx.js → web-BYu5GvMD.js} +1 -1
- package/dist/assets/{web-DBp9A4mr.js → web-BsMMNK0E.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/src/packages/server/integrations/jira/jira-client.js +155 -0
- package/dist/src/packages/server/integrations/jira/jira-routes.js +73 -0
- package/dist/src/packages/server/integrations/jira/jira-skill.js +32 -0
- package/dist/src/packages/server/integrations/slack/slack-client.js +194 -4
- package/dist/src/packages/server/integrations/slack/slack-routes.js +131 -0
- package/dist/src/packages/server/integrations/slack/slack-skill.js +74 -0
- package/dist/src/packages/server/integrations/slack/slack-trigger-handler.js +9 -1
- package/package.json +1 -1
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
* Mounted at /api/slack/ by the integration registry.
|
|
5
5
|
*/
|
|
6
6
|
import { Router } from 'express';
|
|
7
|
+
import multer from 'multer';
|
|
7
8
|
import * as slackClient from './slack-client.js';
|
|
8
9
|
import { loadConfig } from './slack-config.js';
|
|
9
10
|
import { createLogger } from '../../utils/logger.js';
|
|
10
11
|
const log = createLogger('SlackRoutes');
|
|
11
12
|
const router = Router();
|
|
13
|
+
// 50 MB cap matches other integrations (docx). Slack's own limit is higher but this keeps memory sane.
|
|
14
|
+
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
|
|
12
15
|
// POST /api/slack/send — Send a message
|
|
13
16
|
router.post('/send', async (req, res) => {
|
|
14
17
|
try {
|
|
@@ -159,6 +162,134 @@ router.post('/dm', async (req, res) => {
|
|
|
159
162
|
res.status(500).json({ error: `Failed to send DM: ${err instanceof Error ? err.message : err}` });
|
|
160
163
|
}
|
|
161
164
|
});
|
|
165
|
+
// POST /api/slack/upload — Upload a file (multipart/form-data)
|
|
166
|
+
// Fields: file (binary, required), channelId?, title?, initialComment?, threadTs?
|
|
167
|
+
router.post('/upload', upload.single('file'), async (req, res) => {
|
|
168
|
+
try {
|
|
169
|
+
if (!req.file) {
|
|
170
|
+
res.status(400).json({ error: 'No file uploaded. Use multipart/form-data with field name "file".' });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const filename = req.body.filename || req.file.originalname || 'upload.bin';
|
|
174
|
+
const channelId = req.body.channelId;
|
|
175
|
+
const title = req.body.title;
|
|
176
|
+
const initialComment = req.body.initialComment;
|
|
177
|
+
const threadTs = req.body.threadTs;
|
|
178
|
+
const result = await slackClient.uploadFile({
|
|
179
|
+
filename,
|
|
180
|
+
bytes: req.file.buffer,
|
|
181
|
+
channelId,
|
|
182
|
+
title,
|
|
183
|
+
initialComment,
|
|
184
|
+
threadTs,
|
|
185
|
+
});
|
|
186
|
+
res.json({ success: true, fileId: result.fileId, file: result.file });
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
log.error(`Slack upload error: ${err}`);
|
|
190
|
+
res.status(500).json({ error: `Failed to upload file: ${err instanceof Error ? err.message : err}` });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
// POST /api/slack/upload-base64 — Upload a file via JSON (base64-encoded bytes).
|
|
194
|
+
// Body: { filename, contentBase64, channelId?, title?, initialComment?, threadTs? }
|
|
195
|
+
router.post('/upload-base64', async (req, res) => {
|
|
196
|
+
try {
|
|
197
|
+
const { filename, contentBase64, channelId, title, initialComment, threadTs } = req.body;
|
|
198
|
+
if (!filename || !contentBase64) {
|
|
199
|
+
res.status(400).json({ error: 'filename and contentBase64 are required' });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const bytes = Buffer.from(contentBase64, 'base64');
|
|
203
|
+
if (!bytes.length) {
|
|
204
|
+
res.status(400).json({ error: 'contentBase64 decoded to 0 bytes' });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const result = await slackClient.uploadFile({
|
|
208
|
+
filename,
|
|
209
|
+
bytes,
|
|
210
|
+
channelId,
|
|
211
|
+
title,
|
|
212
|
+
initialComment,
|
|
213
|
+
threadTs,
|
|
214
|
+
});
|
|
215
|
+
res.json({ success: true, fileId: result.fileId, file: result.file });
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
log.error(`Slack upload-base64 error: ${err}`);
|
|
219
|
+
res.status(500).json({ error: `Failed to upload file: ${err instanceof Error ? err.message : err}` });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
// GET /api/slack/files — List files (optional filters: channelId, userId, tsFrom, tsTo, types, count, page)
|
|
223
|
+
router.get('/files', async (req, res) => {
|
|
224
|
+
try {
|
|
225
|
+
const count = req.query.count ? parseInt(req.query.count, 10) : undefined;
|
|
226
|
+
const page = req.query.page ? parseInt(req.query.page, 10) : undefined;
|
|
227
|
+
const files = await slackClient.listFiles({
|
|
228
|
+
channelId: req.query.channelId,
|
|
229
|
+
userId: req.query.userId,
|
|
230
|
+
tsFrom: req.query.tsFrom,
|
|
231
|
+
tsTo: req.query.tsTo,
|
|
232
|
+
types: req.query.types,
|
|
233
|
+
count,
|
|
234
|
+
page,
|
|
235
|
+
});
|
|
236
|
+
res.json({ files });
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
log.error(`Slack files list error: ${err}`);
|
|
240
|
+
res.status(500).json({ error: `Failed to list files: ${err instanceof Error ? err.message : err}` });
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
// GET /api/slack/files/:id — Get file metadata
|
|
244
|
+
router.get('/files/:id', async (req, res) => {
|
|
245
|
+
try {
|
|
246
|
+
const file = await slackClient.getFileInfo(req.params.id);
|
|
247
|
+
res.json({ file });
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
log.error(`Slack file info error: ${err}`);
|
|
251
|
+
res.status(500).json({ error: `Failed to get file info: ${err instanceof Error ? err.message : err}` });
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
// GET /api/slack/files/:id/content — Proxy the file's binary content (bot token added server-side)
|
|
255
|
+
router.get('/files/:id/content', async (req, res) => {
|
|
256
|
+
try {
|
|
257
|
+
const { buffer, contentType, contentDisposition, contentLength } = await slackClient.fetchFileBytes(req.params.id);
|
|
258
|
+
if (contentType)
|
|
259
|
+
res.setHeader('Content-Type', contentType);
|
|
260
|
+
if (contentDisposition)
|
|
261
|
+
res.setHeader('Content-Disposition', contentDisposition);
|
|
262
|
+
if (contentLength)
|
|
263
|
+
res.setHeader('Content-Length', contentLength);
|
|
264
|
+
res.status(200).send(buffer);
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
log.error(`Slack file content error: ${err}`);
|
|
268
|
+
res.status(500).json({ error: `Failed to fetch file content: ${err instanceof Error ? err.message : err}` });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
// POST /api/slack/files/:id/download — Server-side download to outputPath on the local filesystem
|
|
272
|
+
router.post('/files/:id/download', async (req, res) => {
|
|
273
|
+
try {
|
|
274
|
+
const { outputPath } = req.body;
|
|
275
|
+
if (!outputPath) {
|
|
276
|
+
res.status(400).json({ error: 'outputPath is required' });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const result = await slackClient.downloadFile(req.params.id, outputPath);
|
|
280
|
+
res.json({
|
|
281
|
+
success: true,
|
|
282
|
+
path: result.path,
|
|
283
|
+
bytes: result.bytes,
|
|
284
|
+
filename: result.filename,
|
|
285
|
+
mimeType: result.mimeType,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
log.error(`Slack file download error: ${err}`);
|
|
290
|
+
res.status(500).json({ error: `Failed to download file: ${err instanceof Error ? err.message : err}` });
|
|
291
|
+
}
|
|
292
|
+
});
|
|
162
293
|
// GET /api/slack/status — Get connection status
|
|
163
294
|
router.get('/status', (_req, res) => {
|
|
164
295
|
const config = loadConfig();
|
|
@@ -95,6 +95,80 @@ curl -s http://localhost:5174/api/slack/channels
|
|
|
95
95
|
curl -s http://localhost:5174/api/slack/users/U0123456789
|
|
96
96
|
\`\`\`
|
|
97
97
|
|
|
98
|
+
## Upload / Images
|
|
99
|
+
|
|
100
|
+
Upload a file or image to Slack. Uses Slack's new two-step files API (files.upload is deprecated since Nov 2025).
|
|
101
|
+
The bot token must have the **\`files:write\`** scope.
|
|
102
|
+
|
|
103
|
+
**Multipart (upload a file from disk):**
|
|
104
|
+
\`\`\`bash
|
|
105
|
+
curl -s -F "file=@image.png" \\
|
|
106
|
+
-F "channelId=C0123456789" \\
|
|
107
|
+
-F "initialComment=Here's the chart" \\
|
|
108
|
+
http://localhost:5174/api/slack/upload
|
|
109
|
+
\`\`\`
|
|
110
|
+
|
|
111
|
+
**JSON / base64 (useful for in-memory images like generated charts):**
|
|
112
|
+
\`\`\`bash
|
|
113
|
+
curl -s -X POST http://localhost:5174/api/slack/upload-base64 \\
|
|
114
|
+
-H "Content-Type: application/json" \\
|
|
115
|
+
-d '{
|
|
116
|
+
"filename":"chart.png",
|
|
117
|
+
"contentBase64":"iVBORw0KGgoAAAANSUhEUg...",
|
|
118
|
+
"channelId":"C0123456789",
|
|
119
|
+
"initialComment":"Here is today\\'s report",
|
|
120
|
+
"threadTs":"1234567890.123456"
|
|
121
|
+
}'
|
|
122
|
+
\`\`\`
|
|
123
|
+
|
|
124
|
+
Fields (both variants):
|
|
125
|
+
- \`file\` or \`contentBase64\` — the bytes (required)
|
|
126
|
+
- \`filename\` — required for base64; multipart uses the uploaded file's original name if omitted
|
|
127
|
+
- \`channelId\` — optional; if omitted the file is uploaded but not shared to a channel
|
|
128
|
+
- \`title\` — optional display title (defaults to filename)
|
|
129
|
+
- \`initialComment\` — optional message posted with the file
|
|
130
|
+
- \`threadTs\` — optional thread timestamp to post the file as a reply
|
|
131
|
+
|
|
132
|
+
Response: \`{"success":true,"fileId":"F0123...","file":{"id","name","title","mimetype","size","permalink","url_private",...}}\`.
|
|
133
|
+
|
|
134
|
+
## Read / Download Files
|
|
135
|
+
|
|
136
|
+
Inspect and download files shared in Slack. Requires the bot token to have **\`files:read\`**.
|
|
137
|
+
|
|
138
|
+
**List files (optional filters):**
|
|
139
|
+
\`\`\`bash
|
|
140
|
+
# All recent files
|
|
141
|
+
curl -s http://localhost:5174/api/slack/files
|
|
142
|
+
|
|
143
|
+
# Images shared in a specific channel
|
|
144
|
+
curl -s "http://localhost:5174/api/slack/files?channelId=C0123456789&types=images&count=20"
|
|
145
|
+
\`\`\`
|
|
146
|
+
Filters: \`channelId\`, \`userId\`, \`tsFrom\`, \`tsTo\`, \`types\` (Slack type string like \`images\`, \`pdfs\`, \`spaces\`), \`count\`, \`page\`.
|
|
147
|
+
|
|
148
|
+
**Get a single file's metadata:**
|
|
149
|
+
\`\`\`bash
|
|
150
|
+
curl -s http://localhost:5174/api/slack/files/F0123ABCD
|
|
151
|
+
\`\`\`
|
|
152
|
+
Returns \`{ "file": { "id","name","title","mimetype","size","permalink","url_private","url_private_download" } }\`.
|
|
153
|
+
|
|
154
|
+
**Download a file (binary proxy, auth added server-side):**
|
|
155
|
+
\`\`\`bash
|
|
156
|
+
curl -s http://localhost:5174/api/slack/files/F0123ABCD/content -o /tmp/attachment.bin
|
|
157
|
+
\`\`\`
|
|
158
|
+
Preserves upstream \`Content-Type\` and \`Content-Disposition\` from Slack's CDN.
|
|
159
|
+
|
|
160
|
+
**Server-side save to a filesystem path:**
|
|
161
|
+
\`\`\`bash
|
|
162
|
+
curl -s -X POST http://localhost:5174/api/slack/files/F0123ABCD/download \\
|
|
163
|
+
-H "Content-Type: application/json" \\
|
|
164
|
+
-d '{"outputPath":"/tmp/slack/F0123ABCD.png"}'
|
|
165
|
+
\`\`\`
|
|
166
|
+
Returns \`{ "success":true, "path":"/tmp/slack/F0123ABCD.png", "bytes": 12345, "filename":"chart.png", "mimeType":"image/png" }\`.
|
|
167
|
+
|
|
168
|
+
Messages returned by \`/messages\` and \`/thread\` now include an optional \`files: [...]\` array on each message when attachments exist — use the file ids there as input to the endpoints above.
|
|
169
|
+
|
|
170
|
+
**Pitfall — do NOT \`curl\` \`url_private\` directly.** Slack's \`url_private\` (and \`url_private_download\`) only return the actual file bytes when the request sends \`Authorization: Bearer <bot-token>\`; without it Slack serves an HTML sign-in page. Use the proxy endpoints above — they attach the bot token server-side so agents never need to handle the token.
|
|
171
|
+
|
|
98
172
|
## Check Connection Status
|
|
99
173
|
|
|
100
174
|
\`\`\`bash
|
|
@@ -46,16 +46,24 @@ export const slackTriggerHandler = {
|
|
|
46
46
|
extractVariables(trigger, event) {
|
|
47
47
|
const msg = event.data;
|
|
48
48
|
void trigger; // trigger config not needed for basic extraction
|
|
49
|
+
const files = msg.files ?? [];
|
|
49
50
|
return {
|
|
50
51
|
'slack.user': msg.userName,
|
|
51
52
|
'slack.userId': msg.userId,
|
|
52
53
|
'slack.message': msg.text,
|
|
53
54
|
'slack.channel': msg.channel,
|
|
54
55
|
'slack.threadTs': msg.threadTs || msg.ts,
|
|
56
|
+
'slack.fileCount': String(files.length),
|
|
57
|
+
'slack.fileIds': files.map((f) => f.id).join(','),
|
|
58
|
+
'slack.fileNames': files.map((f) => f.name ?? '').filter(Boolean).join(','),
|
|
55
59
|
};
|
|
56
60
|
},
|
|
57
61
|
formatEventForLLM(event) {
|
|
58
62
|
const msg = event.data;
|
|
59
|
-
|
|
63
|
+
const files = msg.files ?? [];
|
|
64
|
+
const filesLine = files.length
|
|
65
|
+
? `\nAttachments (${files.length}): ${files.map((f) => `${f.name ?? f.id} [${f.mimetype ?? 'unknown'}]`).join(', ')}`
|
|
66
|
+
: '';
|
|
67
|
+
return `Slack message from @${msg.userName} (${msg.userId}) in #${msg.channel}:\n"${msg.text}"${filesLine}`;
|
|
60
68
|
},
|
|
61
69
|
};
|