@crewx/cli 0.9.0-rc.146 → 0.9.0-rc.147
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/bootstrap/codex-writable-roots.js +1 -113
- package/dist/bootstrap/crewx-cli.js +1 -84
- package/dist/builtin.js +5 -132
- package/dist/commands/agent.js +4 -118
- package/dist/commands/cal.js +2 -44
- package/dist/commands/db.js +4 -602
- package/dist/commands/detached.js +15 -620
- package/dist/commands/doctor.js +4 -649
- package/dist/commands/emit-trailer.js +2 -33
- package/dist/commands/execute.js +15 -294
- package/dist/commands/hook/command-marker.js +1 -5
- package/dist/commands/hook/install.js +8 -189
- package/dist/commands/hook/paths.js +1 -94
- package/dist/commands/hook/status.js +1 -86
- package/dist/commands/hook/uninstall.js +1 -71
- package/dist/commands/hook-dispatch.js +2 -180
- package/dist/commands/init.js +31 -437
- package/dist/commands/kill.js +1 -11
- package/dist/commands/log.js +3 -96
- package/dist/commands/parse-agent-message.js +1 -52
- package/dist/commands/parse-common-flags.js +3 -212
- package/dist/commands/ps.js +2 -117
- package/dist/commands/publish.js +7 -283
- package/dist/commands/query.js +16 -292
- package/dist/commands/registry.js +1 -33
- package/dist/commands/resolve-prompt.js +3 -76
- package/dist/commands/resolve-recipient.js +3 -47
- package/dist/commands/restart.js +18 -172
- package/dist/commands/result.js +1 -209
- package/dist/commands/shortcut.js +4 -253
- package/dist/commands/slack.js +16 -560
- package/dist/commands/stop.js +8 -119
- package/dist/commands/write-output.js +1 -24
- package/dist/index.js +1 -8
- package/dist/logging.js +10 -87
- package/dist/main.js +6 -289
- package/dist/register-builtin-tools.js +1 -9
- package/dist/slack/file-download.js +1 -134
- package/dist/slack/markdown.js +1 -33
- package/dist/utils/env-defaults.js +1 -15
- package/dist/utils/inherited-trace.js +1 -65
- package/dist/utils/product-package-root.js +1 -39
- package/dist/utils/sdk-compat.js +1 -120
- package/dist/utils/version.js +1 -28
- package/package.json +15 -15
package/dist/commands/slack.js
CHANGED
|
@@ -1,560 +1,16 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}) : (function(o, m,
|
|
17
|
-
if (k2 === undefined) k2 = k;
|
|
18
|
-
o[k2] = m[k];
|
|
19
|
-
}));
|
|
20
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
-
}) : function(o, v) {
|
|
23
|
-
o["default"] = v;
|
|
24
|
-
});
|
|
25
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
-
var ownKeys = function(o) {
|
|
27
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
-
var ar = [];
|
|
29
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
-
return ar;
|
|
31
|
-
};
|
|
32
|
-
return ownKeys(o);
|
|
33
|
-
};
|
|
34
|
-
return function (mod) {
|
|
35
|
-
if (mod && mod.__esModule) return mod;
|
|
36
|
-
var result = {};
|
|
37
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
-
__setModuleDefault(result, mod);
|
|
39
|
-
return result;
|
|
40
|
-
};
|
|
41
|
-
})();
|
|
42
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
-
exports.handleSlack = handleSlack;
|
|
44
|
-
const fs = __importStar(require("fs"));
|
|
45
|
-
const path = __importStar(require("path"));
|
|
46
|
-
const https = __importStar(require("https"));
|
|
47
|
-
const sdk_1 = require("@crewx/sdk");
|
|
48
|
-
const adapter_slack_1 = require("@crewx/adapter-slack");
|
|
49
|
-
const crewx_cli_1 = require("../bootstrap/crewx-cli");
|
|
50
|
-
const markdown_1 = require("../slack/markdown");
|
|
51
|
-
const file_download_1 = require("../slack/file-download");
|
|
52
|
-
// ─── Entry point ──────────────────────────────────────────────────────────
|
|
53
|
-
/**
|
|
54
|
-
* Handle `crewx slack` / `crewx slack:files` commands.
|
|
55
|
-
* Subcommand is determined by flags: --list → list, --clean → clean, default → download.
|
|
56
|
-
*/
|
|
57
|
-
async function handleSlack(args) {
|
|
58
|
-
if (args.includes('--mode') || args.includes('--agent')) {
|
|
59
|
-
return await handleBotMode(args);
|
|
60
|
-
}
|
|
61
|
-
try {
|
|
62
|
-
const subCommand = getSubcommand(args);
|
|
63
|
-
const threadId = getThreadOption(args);
|
|
64
|
-
const botToken = process.env.SLACK_BOT_TOKEN;
|
|
65
|
-
if (!botToken && subCommand === 'download') {
|
|
66
|
-
console.error('\n❌ Error: SLACK_BOT_TOKEN environment variable not found\n');
|
|
67
|
-
console.log('Set your Slack bot token:');
|
|
68
|
-
console.log(' export SLACK_BOT_TOKEN=xoxb-your-token\n');
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
switch (subCommand) {
|
|
72
|
-
case 'list':
|
|
73
|
-
await handleListFiles(threadId);
|
|
74
|
-
break;
|
|
75
|
-
case 'clean':
|
|
76
|
-
await handleCleanFiles(threadId);
|
|
77
|
-
break;
|
|
78
|
-
case 'download':
|
|
79
|
-
default:
|
|
80
|
-
await handleDownloadFiles(botToken, threadId);
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
86
|
-
console.error(`\n❌ Slack files command failed:\n${errorMessage}\n`);
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
// ─── Subcommand resolution ───────────────────────────────────────────────
|
|
91
|
-
function getSubcommand(args) {
|
|
92
|
-
if (args.includes('--list') || args.includes('-l'))
|
|
93
|
-
return 'list';
|
|
94
|
-
if (args.includes('--clean'))
|
|
95
|
-
return 'clean';
|
|
96
|
-
return 'download';
|
|
97
|
-
}
|
|
98
|
-
function getThreadOption(args) {
|
|
99
|
-
const idx = args.findIndex(a => a === '--thread' || a === '-t');
|
|
100
|
-
if (idx !== -1 && idx + 1 < args.length) {
|
|
101
|
-
return args[idx + 1];
|
|
102
|
-
}
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
// ─── Download ────────────────────────────────────────────────────────────
|
|
106
|
-
async function handleDownloadFiles(botToken, threadId) {
|
|
107
|
-
if (!threadId) {
|
|
108
|
-
console.error('\n❌ Error: --thread option is required for download\n');
|
|
109
|
-
console.log('Usage: crewx slack:files --thread <thread_ts>\n');
|
|
110
|
-
console.log('Example:');
|
|
111
|
-
console.log(' crewx slack:files --thread 1234567890.123456\n');
|
|
112
|
-
process.exit(1);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const validThreadId = threadId;
|
|
116
|
-
console.log(`\n📥 Downloading files from thread: ${validThreadId}\n`);
|
|
117
|
-
const channelId = validThreadId.includes('.')
|
|
118
|
-
? (validThreadId.split('.')[0] || validThreadId)
|
|
119
|
-
: validThreadId;
|
|
120
|
-
// Fetch conversation history from Slack API
|
|
121
|
-
const messages = await slackConversationsHistory(botToken, channelId, validThreadId);
|
|
122
|
-
if (!messages || messages.length === 0) {
|
|
123
|
-
console.log('⚠️ No messages found in thread\n');
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
// Collect files from messages
|
|
127
|
-
const files = [];
|
|
128
|
-
for (const message of messages) {
|
|
129
|
-
if (message.files) {
|
|
130
|
-
for (const file of message.files) {
|
|
131
|
-
files.push({
|
|
132
|
-
id: file.id,
|
|
133
|
-
name: file.name,
|
|
134
|
-
userId: message.user || 'unknown',
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
if (files.length === 0) {
|
|
140
|
-
console.log('ℹ️ No files found in thread\n');
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
console.log(`Found ${files.length} file${files.length > 1 ? 's' : ''} to download:\n`);
|
|
144
|
-
let downloadedCount = 0;
|
|
145
|
-
let skippedCount = 0;
|
|
146
|
-
let failedCount = 0;
|
|
147
|
-
for (const file of files) {
|
|
148
|
-
try {
|
|
149
|
-
const metadata = await downloadSlackFile(botToken, file.id, file.name, validThreadId, channelId, file.userId);
|
|
150
|
-
const wasSkipped = fs.existsSync(metadata.filePath) &&
|
|
151
|
-
fs.statSync(metadata.filePath).mtime < metadata.downloadedAt;
|
|
152
|
-
if (wasSkipped) {
|
|
153
|
-
console.log(` ⏭️ ${file.name} (already exists)`);
|
|
154
|
-
skippedCount++;
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
console.log(` ✅ ${file.name} (${formatFileSize(metadata.fileSize)})`);
|
|
158
|
-
downloadedCount++;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
catch (error) {
|
|
162
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
163
|
-
console.log(` ❌ ${file.name} - ${errorMessage}`);
|
|
164
|
-
failedCount++;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
console.log(`\n📊 Summary:`);
|
|
168
|
-
if (downloadedCount > 0)
|
|
169
|
-
console.log(` Downloaded: ${downloadedCount} file${downloadedCount > 1 ? 's' : ''}`);
|
|
170
|
-
if (skippedCount > 0)
|
|
171
|
-
console.log(` Skipped: ${skippedCount} file${skippedCount > 1 ? 's' : ''} (already exists)`);
|
|
172
|
-
if (failedCount > 0)
|
|
173
|
-
console.log(` Failed: ${failedCount} file${failedCount > 1 ? 's' : ''}`);
|
|
174
|
-
console.log('');
|
|
175
|
-
}
|
|
176
|
-
// ─── List ────────────────────────────────────────────────────────────────
|
|
177
|
-
async function handleListFiles(threadId) {
|
|
178
|
-
const downloadDir = path.join(process.cwd(), '.crewx', 'slack-files');
|
|
179
|
-
if (threadId) {
|
|
180
|
-
console.log(`\n📋 Files in thread ${threadId}:\n`);
|
|
181
|
-
const files = getThreadFilesMetadata(threadId, downloadDir);
|
|
182
|
-
if (files.length === 0) {
|
|
183
|
-
console.log(' No files downloaded for this thread\n');
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
for (const file of files) {
|
|
187
|
-
console.log(` 📎 ${file.fileName}`);
|
|
188
|
-
console.log(` Size: ${formatFileSize(file.fileSize)}`);
|
|
189
|
-
console.log(` Path: ${file.filePath}`);
|
|
190
|
-
console.log(` Downloaded: ${(0, sdk_1.formatDisplayTimestamp)(file.downloadedAt)}`);
|
|
191
|
-
console.log('');
|
|
192
|
-
}
|
|
193
|
-
console.log(`Total: ${files.length} file${files.length > 1 ? 's' : ''}\n`);
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
console.log('\n📋 All downloaded Slack files:\n');
|
|
197
|
-
if (!fs.existsSync(downloadDir)) {
|
|
198
|
-
console.log(' No files downloaded yet\n');
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
const threadDirs = fs.readdirSync(downloadDir).filter(name => fs.statSync(path.join(downloadDir, name)).isDirectory());
|
|
202
|
-
if (threadDirs.length === 0) {
|
|
203
|
-
console.log(' No files downloaded yet\n');
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
let totalFiles = 0;
|
|
207
|
-
for (const threadDir of threadDirs) {
|
|
208
|
-
const files = getThreadFilesMetadata(threadDir, downloadDir);
|
|
209
|
-
if (files.length > 0) {
|
|
210
|
-
console.log(` Thread: ${threadDir}`);
|
|
211
|
-
for (const file of files) {
|
|
212
|
-
console.log(` 📎 ${file.fileName} (${formatFileSize(file.fileSize)})`);
|
|
213
|
-
totalFiles++;
|
|
214
|
-
}
|
|
215
|
-
console.log('');
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
console.log(`Total: ${totalFiles} file${totalFiles > 1 ? 's' : ''} in ${threadDirs.length} thread${threadDirs.length > 1 ? 's' : ''}\n`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
// ─── Clean ───────────────────────────────────────────────────────────────
|
|
222
|
-
async function handleCleanFiles(threadId) {
|
|
223
|
-
const downloadDir = path.join(process.cwd(), '.crewx', 'slack-files');
|
|
224
|
-
if (threadId) {
|
|
225
|
-
console.log(`\n🧹 Cleaning files for thread ${threadId}...\n`);
|
|
226
|
-
const safeThreadDir = threadId.replace(/:/g, '_');
|
|
227
|
-
const threadDir = path.join(downloadDir, safeThreadDir);
|
|
228
|
-
if (!fs.existsSync(threadDir)) {
|
|
229
|
-
console.log(' No files to clean\n');
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
const files = fs.readdirSync(threadDir);
|
|
233
|
-
for (const file of files) {
|
|
234
|
-
fs.unlinkSync(path.join(threadDir, file));
|
|
235
|
-
}
|
|
236
|
-
fs.rmdirSync(threadDir);
|
|
237
|
-
console.log(`✅ Cleaned ${files.length} file${files.length > 1 ? 's' : ''}\n`);
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
console.log('\n🧹 Cleaning all downloaded files...\n');
|
|
241
|
-
if (!fs.existsSync(downloadDir)) {
|
|
242
|
-
console.log(' No files to clean\n');
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
let totalFiles = 0;
|
|
246
|
-
let totalThreads = 0;
|
|
247
|
-
const threadDirs = fs.readdirSync(downloadDir).filter(name => fs.statSync(path.join(downloadDir, name)).isDirectory());
|
|
248
|
-
for (const threadDir of threadDirs) {
|
|
249
|
-
const threadPath = path.join(downloadDir, threadDir);
|
|
250
|
-
const files = fs.readdirSync(threadPath);
|
|
251
|
-
for (const file of files) {
|
|
252
|
-
fs.unlinkSync(path.join(threadPath, file));
|
|
253
|
-
totalFiles++;
|
|
254
|
-
}
|
|
255
|
-
fs.rmdirSync(threadPath);
|
|
256
|
-
totalThreads++;
|
|
257
|
-
}
|
|
258
|
-
console.log(`✅ Cleaned ${totalFiles} file${totalFiles > 1 ? 's' : ''} from ${totalThreads} thread${totalThreads > 1 ? 's' : ''}\n`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
// ─── Slack API helpers ───────────────────────────────────────────────────
|
|
262
|
-
/**
|
|
263
|
-
* Call Slack conversations.history API using Node.js https.
|
|
264
|
-
* Returns array of message objects.
|
|
265
|
-
*/
|
|
266
|
-
async function slackConversationsHistory(token, channel, latest) {
|
|
267
|
-
const params = new URLSearchParams({
|
|
268
|
-
channel,
|
|
269
|
-
latest,
|
|
270
|
-
inclusive: 'true',
|
|
271
|
-
limit: '100',
|
|
272
|
-
});
|
|
273
|
-
const data = await slackApiGet(token, `conversations.history?${params.toString()}`);
|
|
274
|
-
if (!data.ok) {
|
|
275
|
-
throw new Error(`Slack API error: ${data.error ?? 'unknown_error'}`);
|
|
276
|
-
}
|
|
277
|
-
return data.messages ?? [];
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Download a single Slack file and save it to disk.
|
|
281
|
-
*/
|
|
282
|
-
async function downloadSlackFile(botToken, fileId, fileName, threadId, channelId, userId) {
|
|
283
|
-
// Get file info
|
|
284
|
-
const infoData = await slackApiGet(botToken, `files.info?file=${fileId}`);
|
|
285
|
-
if (!infoData.ok) {
|
|
286
|
-
throw new Error(`Failed to get file info: ${infoData.error ?? 'unknown_error'}`);
|
|
287
|
-
}
|
|
288
|
-
const fileInfo = infoData.file;
|
|
289
|
-
if (!fileInfo?.url_private) {
|
|
290
|
-
throw new Error('No private download URL available for this file');
|
|
291
|
-
}
|
|
292
|
-
// Build save path
|
|
293
|
-
const sanitized = sanitizeFileName(fileName);
|
|
294
|
-
const uniqueName = `${fileId}${path.extname(sanitized)}`;
|
|
295
|
-
const safeThreadDir = threadId.replace(/:/g, '_');
|
|
296
|
-
const downloadDir = path.join(process.cwd(), '.crewx', 'slack-files');
|
|
297
|
-
const savePath = path.join(downloadDir, safeThreadDir, uniqueName);
|
|
298
|
-
// Duplicate check
|
|
299
|
-
if (fs.existsSync(savePath)) {
|
|
300
|
-
const stats = fs.statSync(savePath);
|
|
301
|
-
return {
|
|
302
|
-
fileId,
|
|
303
|
-
fileName: sanitized,
|
|
304
|
-
filePath: savePath,
|
|
305
|
-
fileSize: stats.size,
|
|
306
|
-
mimeType: fileInfo.mimetype ?? 'application/octet-stream',
|
|
307
|
-
uploadedBy: userId,
|
|
308
|
-
uploadedAt: new Date((fileInfo.timestamp ?? 0) * 1000),
|
|
309
|
-
threadId,
|
|
310
|
-
channelId,
|
|
311
|
-
downloadedAt: new Date(stats.mtime),
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
// Fetch file content from Slack private URL
|
|
315
|
-
const fileBuffer = await fetchPrivateUrl(botToken, fileInfo.url_private);
|
|
316
|
-
// Save to disk
|
|
317
|
-
await fs.promises.mkdir(path.dirname(savePath), { recursive: true });
|
|
318
|
-
await fs.promises.writeFile(savePath, new Uint8Array(fileBuffer));
|
|
319
|
-
return {
|
|
320
|
-
fileId,
|
|
321
|
-
fileName: sanitized,
|
|
322
|
-
filePath: savePath,
|
|
323
|
-
fileSize: fileBuffer.length,
|
|
324
|
-
mimeType: fileInfo.mimetype ?? 'application/octet-stream',
|
|
325
|
-
uploadedBy: userId,
|
|
326
|
-
uploadedAt: new Date((fileInfo.timestamp ?? 0) * 1000),
|
|
327
|
-
threadId,
|
|
328
|
-
channelId,
|
|
329
|
-
downloadedAt: new Date(),
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* Make a GET request to a Slack API endpoint.
|
|
334
|
-
*/
|
|
335
|
-
async function slackApiGet(token, endpoint) {
|
|
336
|
-
return new Promise((resolve, reject) => {
|
|
337
|
-
const options = {
|
|
338
|
-
hostname: 'slack.com',
|
|
339
|
-
path: `/api/${endpoint}`,
|
|
340
|
-
method: 'GET',
|
|
341
|
-
headers: {
|
|
342
|
-
Authorization: `Bearer ${token}`,
|
|
343
|
-
'Content-Type': 'application/json',
|
|
344
|
-
},
|
|
345
|
-
};
|
|
346
|
-
const req = https.request(options, res => {
|
|
347
|
-
let body = '';
|
|
348
|
-
res.on('data', chunk => { body += chunk; });
|
|
349
|
-
res.on('end', () => {
|
|
350
|
-
try {
|
|
351
|
-
resolve(JSON.parse(body));
|
|
352
|
-
}
|
|
353
|
-
catch {
|
|
354
|
-
reject(new Error(`Failed to parse Slack API response: ${body}`));
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
});
|
|
358
|
-
req.on('error', reject);
|
|
359
|
-
req.setTimeout(30000, () => {
|
|
360
|
-
req.destroy();
|
|
361
|
-
reject(new Error('Slack API request timed out'));
|
|
362
|
-
});
|
|
363
|
-
req.end();
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
/**
|
|
367
|
-
* Download content from a Slack private URL using Bearer auth.
|
|
368
|
-
*/
|
|
369
|
-
async function fetchPrivateUrl(token, url) {
|
|
370
|
-
const parsed = new URL(url);
|
|
371
|
-
return new Promise((resolve, reject) => {
|
|
372
|
-
const options = {
|
|
373
|
-
hostname: parsed.hostname,
|
|
374
|
-
path: parsed.pathname + parsed.search,
|
|
375
|
-
method: 'GET',
|
|
376
|
-
headers: {
|
|
377
|
-
Authorization: `Bearer ${token}`,
|
|
378
|
-
},
|
|
379
|
-
};
|
|
380
|
-
const req = https.request(options, res => {
|
|
381
|
-
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
382
|
-
// Follow redirect
|
|
383
|
-
const location = res.headers.location;
|
|
384
|
-
if (!location) {
|
|
385
|
-
reject(new Error('Redirect without location header'));
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
fetchPrivateUrl(token, location).then(resolve).catch(reject);
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if (res.statusCode !== 200) {
|
|
392
|
-
reject(new Error(`HTTP ${res.statusCode} while downloading file`));
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
const chunks = [];
|
|
396
|
-
res.on('data', (chunk) => chunks.push(chunk));
|
|
397
|
-
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
398
|
-
res.on('error', reject);
|
|
399
|
-
});
|
|
400
|
-
req.on('error', reject);
|
|
401
|
-
req.setTimeout(30000, () => {
|
|
402
|
-
req.destroy();
|
|
403
|
-
reject(new Error('File download timed out'));
|
|
404
|
-
});
|
|
405
|
-
req.end();
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
// ─── Local file helpers ───────────────────────────────────────────────────
|
|
409
|
-
function getThreadFilesMetadata(threadId, downloadDir) {
|
|
410
|
-
const safeThreadDir = threadId.replace(/:/g, '_');
|
|
411
|
-
const threadDir = path.join(downloadDir, safeThreadDir);
|
|
412
|
-
if (!fs.existsSync(threadDir))
|
|
413
|
-
return [];
|
|
414
|
-
const files = fs.readdirSync(threadDir);
|
|
415
|
-
const metadata = [];
|
|
416
|
-
for (const file of files) {
|
|
417
|
-
try {
|
|
418
|
-
const filePath = path.join(threadDir, file);
|
|
419
|
-
const stats = fs.statSync(filePath);
|
|
420
|
-
metadata.push({
|
|
421
|
-
fileId: '',
|
|
422
|
-
fileName: file,
|
|
423
|
-
filePath,
|
|
424
|
-
fileSize: stats.size,
|
|
425
|
-
mimeType: guessMimeType(file),
|
|
426
|
-
uploadedBy: '',
|
|
427
|
-
uploadedAt: new Date(stats.birthtime),
|
|
428
|
-
threadId,
|
|
429
|
-
channelId: '',
|
|
430
|
-
downloadedAt: new Date(stats.mtime),
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
catch {
|
|
434
|
-
// ignore unreadable files
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
return metadata;
|
|
438
|
-
}
|
|
439
|
-
function sanitizeFileName(fileName) {
|
|
440
|
-
let safeName = fileName.replace(/[/\\]/g, '_');
|
|
441
|
-
safeName = safeName.replace(/\.\./g, '_');
|
|
442
|
-
safeName = safeName.replace(/[\x00-\x1f\x80-\x9f<>:"|?*]/g, '_');
|
|
443
|
-
safeName = safeName.replace(/^\.+|\.+$/g, '');
|
|
444
|
-
safeName = safeName.trim();
|
|
445
|
-
if (!safeName)
|
|
446
|
-
safeName = 'unnamed_file';
|
|
447
|
-
return safeName;
|
|
448
|
-
}
|
|
449
|
-
function guessMimeType(fileName) {
|
|
450
|
-
const ext = path.extname(fileName).toLowerCase();
|
|
451
|
-
const mimeTypes = {
|
|
452
|
-
'.pdf': 'application/pdf',
|
|
453
|
-
'.png': 'image/png',
|
|
454
|
-
'.jpg': 'image/jpeg',
|
|
455
|
-
'.jpeg': 'image/jpeg',
|
|
456
|
-
'.gif': 'image/gif',
|
|
457
|
-
'.txt': 'text/plain',
|
|
458
|
-
'.md': 'text/markdown',
|
|
459
|
-
'.csv': 'text/csv',
|
|
460
|
-
'.json': 'application/json',
|
|
461
|
-
'.xml': 'application/xml',
|
|
462
|
-
'.zip': 'application/zip',
|
|
463
|
-
'.doc': 'application/msword',
|
|
464
|
-
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
465
|
-
'.xls': 'application/vnd.ms-excel',
|
|
466
|
-
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
467
|
-
};
|
|
468
|
-
return mimeTypes[ext] ?? 'application/octet-stream';
|
|
469
|
-
}
|
|
470
|
-
function formatFileSize(bytes) {
|
|
471
|
-
if (bytes < 1024)
|
|
472
|
-
return `${bytes}B`;
|
|
473
|
-
if (bytes < 1024 * 1024)
|
|
474
|
-
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
475
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
476
|
-
}
|
|
477
|
-
// ─── Bot mode ────────────────────────────────────────────────────────────
|
|
478
|
-
function parseFlag(args, flag) {
|
|
479
|
-
const idx = args.indexOf(flag);
|
|
480
|
-
if (idx !== -1 && idx + 1 < args.length) {
|
|
481
|
-
return args[idx + 1];
|
|
482
|
-
}
|
|
483
|
-
const prefix = `${flag}=`;
|
|
484
|
-
const found = args.find(a => a.startsWith(prefix));
|
|
485
|
-
return found ? found.slice(prefix.length) : undefined;
|
|
486
|
-
}
|
|
487
|
-
async function handleBotMode(args) {
|
|
488
|
-
const mode = parseFlag(args, '--mode') ?? 'query';
|
|
489
|
-
const agent = parseFlag(args, '--agent');
|
|
490
|
-
if (!agent) {
|
|
491
|
-
console.error('Error: --agent required for bot mode');
|
|
492
|
-
console.error('Usage: crewx slack --mode <query|execute> --agent <agent-id>');
|
|
493
|
-
process.exit(1);
|
|
494
|
-
}
|
|
495
|
-
if (mode !== 'query' && mode !== 'execute') {
|
|
496
|
-
console.error(`Error: invalid mode "${mode}" — must be query or execute`);
|
|
497
|
-
process.exit(1);
|
|
498
|
-
}
|
|
499
|
-
const botToken = process.env.SLACK_BOT_TOKEN;
|
|
500
|
-
const appToken = process.env.SLACK_APP_TOKEN;
|
|
501
|
-
if (!botToken || !appToken) {
|
|
502
|
-
console.error('Error: SLACK_BOT_TOKEN and SLACK_APP_TOKEN environment variables required');
|
|
503
|
-
process.exit(1);
|
|
504
|
-
}
|
|
505
|
-
const configPath = process.env.CREWX_CONFIG ?? './crewx.yaml';
|
|
506
|
-
let crewx;
|
|
507
|
-
try {
|
|
508
|
-
crewx = await (0, crewx_cli_1.createCliCrewx)(configPath);
|
|
509
|
-
}
|
|
510
|
-
catch (err) {
|
|
511
|
-
console.error(`Failed to load config: ${configPath}`);
|
|
512
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
513
|
-
process.exit(1);
|
|
514
|
-
}
|
|
515
|
-
const adapter = new adapter_slack_1.SlackAdapter();
|
|
516
|
-
await crewx.registerChannelAdapter({
|
|
517
|
-
adapter,
|
|
518
|
-
instanceId: `slack-${agent}`,
|
|
519
|
-
config: { mode: 'socket', botToken, appToken, myAgentId: agent },
|
|
520
|
-
defaultMode: mode,
|
|
521
|
-
defaultAgent: agent,
|
|
522
|
-
onInbound: async (msg) => {
|
|
523
|
-
if (msg.attachments && msg.attachments.length > 0) {
|
|
524
|
-
try {
|
|
525
|
-
await (0, file_download_1.downloadInboundFiles)(botToken, msg.attachments.map(a => ({
|
|
526
|
-
url_private: a.url,
|
|
527
|
-
mimetype: a.mimeType ?? 'application/octet-stream',
|
|
528
|
-
name: a.name ?? 'unnamed',
|
|
529
|
-
})), msg.threadId);
|
|
530
|
-
}
|
|
531
|
-
catch (err) {
|
|
532
|
-
console.error('File download failed:', err instanceof Error ? err.message : String(err));
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
const fn = mode === 'execute' ? crewx.execute.bind(crewx) : crewx.query.bind(crewx);
|
|
536
|
-
const result = await fn(`@${agent}`, msg.text, { threadId: msg.threadId, platform: adapter.manifest.platform });
|
|
537
|
-
const rawOutput = result.ok
|
|
538
|
-
? result.data
|
|
539
|
-
: (result.error?.message ?? 'Error');
|
|
540
|
-
return { accepted: true, output: (0, markdown_1.markdownToMrkdwn)(rawOutput) };
|
|
541
|
-
},
|
|
542
|
-
});
|
|
543
|
-
console.log(`Slack bot started (agent=${agent}, mode=${mode}, instance=slack-${agent})`);
|
|
544
|
-
console.log('Press Ctrl+C to shut down.');
|
|
545
|
-
await new Promise((resolve) => {
|
|
546
|
-
const shutdown = async () => {
|
|
547
|
-
console.log('\nShutting down...');
|
|
548
|
-
try {
|
|
549
|
-
await crewx.stopAllAdapters({ timeoutMs: 5000 });
|
|
550
|
-
}
|
|
551
|
-
catch (err) {
|
|
552
|
-
console.error('Shutdown error:', err instanceof Error ? err.message : String(err));
|
|
553
|
-
}
|
|
554
|
-
resolve();
|
|
555
|
-
};
|
|
556
|
-
process.once('SIGINT', shutdown);
|
|
557
|
-
process.once('SIGTERM', shutdown);
|
|
558
|
-
});
|
|
559
|
-
process.exit(0);
|
|
560
|
-
}
|
|
1
|
+
"use strict";var k=exports&&exports.__createBinding||(Object.create?(function(n,e,o,t){if(t===void 0)t=o;var r=Object.getOwnPropertyDescriptor(e,o);if(!r||("get"in r?!e.__esModule:r.writable||r.configurable)){r={enumerable:true,get:function(){return e[o]}}}Object.defineProperty(n,t,r)}):(function(n,e,o,t){if(t===void 0)t=o;n[t]=e[o]}));var v=exports&&exports.__setModuleDefault||(Object.create?(function(n,e){Object.defineProperty(n,"default",{enumerable:true,value:e})}):function(n,e){n["default"]=e});var h=exports&&exports.__importStar||(function(){var n=function(e){n=Object.getOwnPropertyNames||function(o){var t=[];for(var r in o)if(Object.prototype.hasOwnProperty.call(o,r))t[t.length]=r;return t};return n(e)};return function(e){if(e&&e.__esModule)return e;var o={};if(e!=null){for(var t=n(e),r=0;r<t.length;r++)if(t[r]!=="default")k(o,e,t[r])}v(o,e);return o}})();Object.defineProperty(exports,"__esModule",{value:true});exports.handleSlack=O;const d=h(require("fs"));const u=h(require("path"));const S=h(require("https"));const A=require("@crewx/sdk");const E=require("@crewx/adapter-slack");const C=require("../bootstrap/crewx-cli");const P=require("../slack/markdown");const D=require("../slack/file-download");async function O(n){if(n.includes("--mode")||n.includes("--agent")){return await L(n)}try{const e=b(n);const o=N(n);const t=process.env.SLACK_BOT_TOKEN;if(!t&&e==="download"){console.error("\n\u274C Error: SLACK_BOT_TOKEN environment variable not found\n");console.log("Set your Slack bot token:");console.log(" export SLACK_BOT_TOKEN=xoxb-your-token\n");process.exit(1)}switch(e){case"list":await q(o);break;case"clean":await j(o);break;case"download":default:await F(t,o);break}}catch(e){const o=e instanceof Error?e.message:String(e);console.error(`
|
|
2
|
+
\u274C Slack files command failed:
|
|
3
|
+
${o}
|
|
4
|
+
`);process.exit(1)}}function b(n){if(n.includes("--list")||n.includes("-l"))return"list";if(n.includes("--clean"))return"clean";return"download"}function N(n){const e=n.findIndex(o=>o==="--thread"||o==="-t");if(e!==-1&&e+1<n.length){return n[e+1]}return void 0}async function F(n,e){if(!e){console.error("\n\u274C Error: --thread option is required for download\n");console.log("Usage: crewx slack:files --thread <thread_ts>\n");console.log("Example:");console.log(" crewx slack:files --thread 1234567890.123456\n");process.exit(1);return}const o=e;console.log(`
|
|
5
|
+
\u{1F4E5} Downloading files from thread: ${o}
|
|
6
|
+
`);const t=o.includes(".")?o.split(".")[0]||o:o;const r=await B(n,t,o);if(!r||r.length===0){console.log("\u26A0\uFE0F No messages found in thread\n");return}const a=[];for(const c of r){if(c.files){for(const f of c.files){a.push({id:f.id,name:f.name,userId:c.user||"unknown"})}}}if(a.length===0){console.log("\u2139\uFE0F No files found in thread\n");return}console.log(`Found ${a.length} file${a.length>1?"s":""} to download:
|
|
7
|
+
`);let i=0;let s=0;let l=0;for(const c of a){try{const f=await z(n,c.id,c.name,o,t,c.userId);const m=d.existsSync(f.filePath)&&d.statSync(f.filePath).mtime<f.downloadedAt;if(m){console.log(` \u23ED\uFE0F ${c.name} (already exists)`);s++}else{console.log(` \u2705 ${c.name} (${g(f.fileSize)})`);i++}}catch(f){const m=f instanceof Error?f.message:String(f);console.log(` \u274C ${c.name} - ${m}`);l++}}console.log(`
|
|
8
|
+
\u{1F4CA} Summary:`);if(i>0)console.log(` Downloaded: ${i} file${i>1?"s":""}`);if(s>0)console.log(` Skipped: ${s} file${s>1?"s":""} (already exists)`);if(l>0)console.log(` Failed: ${l} file${l>1?"s":""}`);console.log("")}async function q(n){const e=u.join(process.cwd(),".crewx","slack-files");if(n){console.log(`
|
|
9
|
+
\u{1F4CB} Files in thread ${n}:
|
|
10
|
+
`);const o=_(n,e);if(o.length===0){console.log(" No files downloaded for this thread\n");return}for(const t of o){console.log(` \u{1F4CE} ${t.fileName}`);console.log(` Size: ${g(t.fileSize)}`);console.log(` Path: ${t.filePath}`);console.log(` Downloaded: ${(0,A.formatDisplayTimestamp)(t.downloadedAt)}`);console.log("")}console.log(`Total: ${o.length} file${o.length>1?"s":""}
|
|
11
|
+
`)}else{console.log("\n\u{1F4CB} All downloaded Slack files:\n");if(!d.existsSync(e)){console.log(" No files downloaded yet\n");return}const o=d.readdirSync(e).filter(r=>d.statSync(u.join(e,r)).isDirectory());if(o.length===0){console.log(" No files downloaded yet\n");return}let t=0;for(const r of o){const a=_(r,e);if(a.length>0){console.log(` Thread: ${r}`);for(const i of a){console.log(` \u{1F4CE} ${i.fileName} (${g(i.fileSize)})`);t++}console.log("")}}console.log(`Total: ${t} file${t>1?"s":""} in ${o.length} thread${o.length>1?"s":""}
|
|
12
|
+
`)}}async function j(n){const e=u.join(process.cwd(),".crewx","slack-files");if(n){console.log(`
|
|
13
|
+
\u{1F9F9} Cleaning files for thread ${n}...
|
|
14
|
+
`);const o=n.replace(/:/g,"_");const t=u.join(e,o);if(!d.existsSync(t)){console.log(" No files to clean\n");return}const r=d.readdirSync(t);for(const a of r){d.unlinkSync(u.join(t,a))}d.rmdirSync(t);console.log(`\u2705 Cleaned ${r.length} file${r.length>1?"s":""}
|
|
15
|
+
`)}else{console.log("\n\u{1F9F9} Cleaning all downloaded files...\n");if(!d.existsSync(e)){console.log(" No files to clean\n");return}let o=0;let t=0;const r=d.readdirSync(e).filter(a=>d.statSync(u.join(e,a)).isDirectory());for(const a of r){const i=u.join(e,a);const s=d.readdirSync(i);for(const l of s){d.unlinkSync(u.join(i,l));o++}d.rmdirSync(i);t++}console.log(`\u2705 Cleaned ${o} file${o>1?"s":""} from ${t} thread${t>1?"s":""}
|
|
16
|
+
`)}}async function B(n,e,o){const t=new URLSearchParams({channel:e,latest:o,inclusive:"true",limit:"100"});const r=await $(n,`conversations.history?${t.toString()}`);if(!r.ok){throw new Error(`Slack API error: ${r.error??"unknown_error"}`)}return r.messages??[]}async function z(n,e,o,t,r,a){const i=await $(n,`files.info?file=${e}`);if(!i.ok){throw new Error(`Failed to get file info: ${i.error??"unknown_error"}`)}const s=i.file;if(!s?.url_private){throw new Error("No private download URL available for this file")}const l=K(o);const c=`${e}${u.extname(l)}`;const f=t.replace(/:/g,"_");const m=u.join(process.cwd(),".crewx","slack-files");const p=u.join(m,f,c);if(d.existsSync(p)){const y=d.statSync(p);return{fileId:e,fileName:l,filePath:p,fileSize:y.size,mimeType:s.mimetype??"application/octet-stream",uploadedBy:a,uploadedAt:new Date((s.timestamp??0)*1e3),threadId:t,channelId:r,downloadedAt:new Date(y.mtime)}}const w=await x(n,s.url_private);await d.promises.mkdir(u.dirname(p),{recursive:true});await d.promises.writeFile(p,new Uint8Array(w));return{fileId:e,fileName:l,filePath:p,fileSize:w.length,mimeType:s.mimetype??"application/octet-stream",uploadedBy:a,uploadedAt:new Date((s.timestamp??0)*1e3),threadId:t,channelId:r,downloadedAt:new Date}}async function $(n,e){return new Promise((o,t)=>{const r={hostname:"slack.com",path:`/api/${e}`,method:"GET",headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json"}};const a=S.request(r,i=>{let s="";i.on("data",l=>{s+=l});i.on("end",()=>{try{o(JSON.parse(s))}catch{t(new Error(`Failed to parse Slack API response: ${s}`))}})});a.on("error",t);a.setTimeout(3e4,()=>{a.destroy();t(new Error("Slack API request timed out"))});a.end()})}async function x(n,e){const o=new URL(e);return new Promise((t,r)=>{const a={hostname:o.hostname,path:o.pathname+o.search,method:"GET",headers:{Authorization:`Bearer ${n}`}};const i=S.request(a,s=>{if(s.statusCode===302||s.statusCode===301){const c=s.headers.location;if(!c){r(new Error("Redirect without location header"));return}x(n,c).then(t).catch(r);return}if(s.statusCode!==200){r(new Error(`HTTP ${s.statusCode} while downloading file`));return}const l=[];s.on("data",c=>l.push(c));s.on("end",()=>t(Buffer.concat(l)));s.on("error",r)});i.on("error",r);i.setTimeout(3e4,()=>{i.destroy();r(new Error("File download timed out"))});i.end()})}function _(n,e){const o=n.replace(/:/g,"_");const t=u.join(e,o);if(!d.existsSync(t))return[];const r=d.readdirSync(t);const a=[];for(const i of r){try{const s=u.join(t,i);const l=d.statSync(s);a.push({fileId:"",fileName:i,filePath:s,fileSize:l.size,mimeType:M(i),uploadedBy:"",uploadedAt:new Date(l.birthtime),threadId:n,channelId:"",downloadedAt:new Date(l.mtime)})}catch{}}return a}function K(n){let e=n.replace(/[/\\]/g,"_");e=e.replace(/\.\./g,"_");e=e.replace(/[\x00-\x1f\x80-\x9f<>:"|?*]/g,"_");e=e.replace(/^\.+|\.+$/g,"");e=e.trim();if(!e)e="unnamed_file";return e}function M(n){const e=u.extname(n).toLowerCase();const o={".pdf":"application/pdf",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".txt":"text/plain",".md":"text/markdown",".csv":"text/csv",".json":"application/json",".xml":"application/xml",".zip":"application/zip",".doc":"application/msword",".docx":"application/vnd.openxmlformats-officedocument.wordprocessingml.document",".xls":"application/vnd.ms-excel",".xlsx":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"};return o[e]??"application/octet-stream"}function g(n){if(n<1024)return`${n}B`;if(n<1024*1024)return`${(n/1024).toFixed(1)}KB`;return`${(n/(1024*1024)).toFixed(1)}MB`}function T(n,e){const o=n.indexOf(e);if(o!==-1&&o+1<n.length){return n[o+1]}const t=`${e}=`;const r=n.find(a=>a.startsWith(t));return r?r.slice(t.length):void 0}async function L(n){const e=T(n,"--mode")??"query";const o=T(n,"--agent");if(!o){console.error("Error: --agent required for bot mode");console.error("Usage: crewx slack --mode <query|execute> --agent <agent-id>");process.exit(1)}if(e!=="query"&&e!=="execute"){console.error(`Error: invalid mode "${e}" \u2014 must be query or execute`);process.exit(1)}const t=process.env.SLACK_BOT_TOKEN;const r=process.env.SLACK_APP_TOKEN;if(!t||!r){console.error("Error: SLACK_BOT_TOKEN and SLACK_APP_TOKEN environment variables required");process.exit(1)}const a=process.env.CREWX_CONFIG??"./crewx.yaml";let i;try{i=await(0,C.createCliCrewx)(a)}catch(l){console.error(`Failed to load config: ${a}`);console.error(l instanceof Error?l.message:String(l));process.exit(1)}const s=new E.SlackAdapter;await i.registerChannelAdapter({adapter:s,instanceId:`slack-${o}`,config:{mode:"socket",botToken:t,appToken:r,myAgentId:o},defaultMode:e,defaultAgent:o,onInbound:async l=>{if(l.attachments&&l.attachments.length>0){try{await(0,D.downloadInboundFiles)(t,l.attachments.map(p=>({url_private:p.url,mimetype:p.mimeType??"application/octet-stream",name:p.name??"unnamed"})),l.threadId)}catch(p){console.error("File download failed:",p instanceof Error?p.message:String(p))}}const c=e==="execute"?i.execute.bind(i):i.query.bind(i);const f=await c(`@${o}`,l.text,{threadId:l.threadId,platform:s.manifest.platform});const m=f.ok?f.data:f.error?.message??"Error";return{accepted:true,output:(0,P.markdownToMrkdwn)(m)}}});console.log(`Slack bot started (agent=${o}, mode=${e}, instance=slack-${o})`);console.log("Press Ctrl+C to shut down.");await new Promise(l=>{const c=async()=>{console.log("\nShutting down...");try{await i.stopAllAdapters({timeoutMs:5e3})}catch(f){console.error("Shutdown error:",f instanceof Error?f.message:String(f))}l()};process.once("SIGINT",c);process.once("SIGTERM",c)});process.exit(0)}
|