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.
Files changed (40) hide show
  1. package/dist/assets/{BossLogsModal-Cc26KWtF.js → BossLogsModal-BpNtzCTX.js} +1 -1
  2. package/dist/assets/{BossSpawnModal-79zixDGu.js → BossSpawnModal-ByiEkNZl.js} +1 -1
  3. package/dist/assets/{ControlsModal-sIxaSPTd.js → ControlsModal-BgeLGCGT.js} +1 -1
  4. package/dist/assets/{DockerLogsModal-RAIqQDHy.js → DockerLogsModal-CaK9YJT4.js} +1 -1
  5. package/dist/assets/{EmbeddedEditor-SYtH65mY.js → EmbeddedEditor-DcGJCiuP.js} +1 -1
  6. package/dist/assets/{GmailOAuthSetup-DK2GlEv1.js → GmailOAuthSetup-CYIBkOXn.js} +1 -1
  7. package/dist/assets/{GoogleOAuthSetup-CprVeTSW.js → GoogleOAuthSetup-CY_EF39t.js} +1 -1
  8. package/dist/assets/{IframeModal-pzXWPx2i.js → IframeModal-DDFIBCq1.js} +1 -1
  9. package/dist/assets/{IntegrationsPanel-BzPNGPs-.js → IntegrationsPanel-CssC1pup.js} +2 -2
  10. package/dist/assets/{LogViewerModal-oqzbJ5iy.js → LogViewerModal-C_f0Z-2y.js} +1 -1
  11. package/dist/assets/{MonitoringModal-DadiFtmu.js → MonitoringModal-CkOkdUvI.js} +1 -1
  12. package/dist/assets/{PM2LogsModal-C4hQRREX.js → PM2LogsModal-BtqM8TkL.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-fHOJ9ZwM.js → RestoreArchivedAreaModal-CIqWIO9m.js} +1 -1
  14. package/dist/assets/{Scene2DCanvas-BeHXBqTf.js → Scene2DCanvas-CJ_MZU-J.js} +1 -1
  15. package/dist/assets/{SceneManager-B6E_cAYg.js → SceneManager-DyX0JqRQ.js} +1 -1
  16. package/dist/assets/{SkillsPanel-B3-ni6t4.js → SkillsPanel-DR7WNV5l.js} +1 -1
  17. package/dist/assets/{SpawnModal-00Lv0ZQy.js → SpawnModal-BCJDoSQg.js} +1 -1
  18. package/dist/assets/{SubordinateAssignmentModal-DVgwAkbS.js → SubordinateAssignmentModal-DAlLWBtv.js} +1 -1
  19. package/dist/assets/{TriggerManagerPanel-GUPU3sup.js → TriggerManagerPanel-ChBctjk3.js} +1 -1
  20. package/dist/assets/{WorkflowEditorPanel-CP26YA9V.js → WorkflowEditorPanel-Bk-QD7-7.js} +1 -1
  21. package/dist/assets/{index-alq8vlxv.js → index-COaYNxnC.js} +1 -1
  22. package/dist/assets/{index-DwJ7OcGh.js → index-CVIkvsfJ.js} +1 -1
  23. package/dist/assets/{index-B9SzB9ov.js → index-DcxPBYWc.js} +1 -1
  24. package/dist/assets/{index-BS2yVyno.js → index-DoOLLVEG.js} +2 -2
  25. package/dist/assets/{index-DLkUpBdu.js → index-DwTs55lG.js} +1 -1
  26. package/dist/assets/{index-C7PPAB37.js → index-Dwt45QE5.js} +1 -1
  27. package/dist/assets/{index-D8tCoR93.js → index-M-tYhxQp.js} +3 -3
  28. package/dist/assets/{index-DKziSiqq.js → index-vx9STDIr.js} +1 -1
  29. package/dist/assets/{main-DxFgIntP.js → main-5l14ueeE.js} +4 -4
  30. package/dist/assets/{web-BGvcteqx.js → web-BYu5GvMD.js} +1 -1
  31. package/dist/assets/{web-DBp9A4mr.js → web-BsMMNK0E.js} +1 -1
  32. package/dist/index.html +1 -1
  33. package/dist/src/packages/server/integrations/jira/jira-client.js +155 -0
  34. package/dist/src/packages/server/integrations/jira/jira-routes.js +73 -0
  35. package/dist/src/packages/server/integrations/jira/jira-skill.js +32 -0
  36. package/dist/src/packages/server/integrations/slack/slack-client.js +194 -4
  37. package/dist/src/packages/server/integrations/slack/slack-routes.js +131 -0
  38. package/dist/src/packages/server/integrations/slack/slack-skill.js +74 -0
  39. package/dist/src/packages/server/integrations/slack/slack-trigger-handler.js +9 -1
  40. 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
- return `Slack message from @${msg.userName} (${msg.userId}) in #${msg.channel}:\n"${msg.text}"`;
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "1.62.0",
3
+ "version": "1.63.0",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",