agent-window 1.3.9 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/settings.json +67 -0
- package/install-bmad-automated.exp +91 -0
- package/install-bmad-interactive.sh +22 -0
- package/install-bmad.js +148 -0
- package/package.json +6 -20
- package/src/api/routes/instances.js +58 -15
- package/src/api/routes/operations.js +40 -10
- package/src/bmad/experts.js +165 -0
- package/src/bmad/formatter.js +238 -0
- package/src/bmad/index.js +10 -0
- package/src/bmad/nlp.js +189 -0
- package/src/bmad/participants.js +148 -0
- package/src/bot.js +331 -40
- package/src/core/config.js +19 -2
- package/src/core/instance/manager.js +59 -1
- package/src/core/mcp-sync.js +114 -0
- package/src/core/perf-monitor.js +1 -1
- package/src/core/platform/pm2-bridge.js +50 -24
- package/src/module-help.csv +4 -0
- package/src/module.yaml +46 -0
- package/web/dist/assets/{Dashboard-323Iwz70.css → Dashboard-Db69RRgS.css} +1 -1
- package/web/dist/assets/Dashboard-jlJe-uLN.js +1 -0
- package/web/dist/assets/{InstanceDetail-d8jMzT8_.js → InstanceDetail-BwYesO8h.js} +1 -1
- package/web/dist/assets/{Instances-B9UMgfY_.js → Instances-eF1JJARO.js} +1 -1
- package/web/dist/assets/{Settings-CxVYMUOR.js → Settings-BFnkGSaK.js} +1 -1
- package/web/dist/assets/{main-dB-FsGdD.js → main-D6Y4CFNn.js} +2 -2
- package/web/dist/index.html +1 -1
- package/web/dist/assets/Dashboard-CkSgP5p6.js +0 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BMAD Participant Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages active experts per Discord channel (meeting room metaphor)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getDefaultExpert, getExpert } from './experts.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Participant Manager class
|
|
11
|
+
* Tracks active experts per channel with Master fallback
|
|
12
|
+
*/
|
|
13
|
+
export class ParticipantManager {
|
|
14
|
+
constructor() {
|
|
15
|
+
// Map<channelId, Map<expertName, expert>>
|
|
16
|
+
this.channels = new Map();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get participants for a channel
|
|
21
|
+
* @param {string} channelId - Discord channel ID
|
|
22
|
+
* @returns {Array} Array of expert objects in the channel
|
|
23
|
+
*/
|
|
24
|
+
getParticipants(channelId) {
|
|
25
|
+
const channel = this.channels.get(channelId);
|
|
26
|
+
if (!channel) return [];
|
|
27
|
+
return Array.from(channel.values());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if channel has any participants
|
|
32
|
+
* @param {string} channelId - Discord channel ID
|
|
33
|
+
* @returns {boolean} True if channel has participants
|
|
34
|
+
*/
|
|
35
|
+
hasParticipants(channelId) {
|
|
36
|
+
const channel = this.channels.get(channelId);
|
|
37
|
+
return channel && channel.size > 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Add participant to channel
|
|
42
|
+
* @param {string} channelId - Discord channel ID
|
|
43
|
+
* @param {Object} expert - Expert object to add
|
|
44
|
+
* @returns {Object} { added: boolean }
|
|
45
|
+
*/
|
|
46
|
+
addParticipant(channelId, expert) {
|
|
47
|
+
if (!expert || !expert.name) return { added: false };
|
|
48
|
+
|
|
49
|
+
if (!this.channels.has(channelId)) {
|
|
50
|
+
this.channels.set(channelId, new Map());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const channel = this.channels.get(channelId);
|
|
54
|
+
|
|
55
|
+
if (channel.has(expert.name)) {
|
|
56
|
+
return { added: false }; // Already in room
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
channel.set(expert.name, expert);
|
|
60
|
+
console.log(`[BMAD] Added ${expert.displayName || expert.name} to channel ${channelId}`);
|
|
61
|
+
return { added: true };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Remove participant from channel
|
|
66
|
+
* Enforces Master fallback rule: room cannot be empty
|
|
67
|
+
* @param {string} channelId - Discord channel ID
|
|
68
|
+
* @param {string} expertName - Expert name to remove
|
|
69
|
+
* @returns {Object} { removed: boolean, fallback: boolean, message: string }
|
|
70
|
+
*/
|
|
71
|
+
removeParticipant(channelId, expertName) {
|
|
72
|
+
const channel = this.channels.get(channelId);
|
|
73
|
+
if (!channel) {
|
|
74
|
+
return { removed: false, fallback: false, message: 'Channel has no participants' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!channel.has(expertName)) {
|
|
78
|
+
return { removed: false, fallback: false, message: 'Expert not in room' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check if this is the last participant
|
|
82
|
+
if (channel.size === 1) {
|
|
83
|
+
const master = getDefaultExpert();
|
|
84
|
+
|
|
85
|
+
// If trying to remove Master and they're the only one
|
|
86
|
+
if (channel.has(master?.name)) {
|
|
87
|
+
return {
|
|
88
|
+
removed: false,
|
|
89
|
+
fallback: false,
|
|
90
|
+
message: 'Cannot dismiss Master - room cannot be empty'
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Remove the expert but add Master
|
|
95
|
+
channel.delete(expertName);
|
|
96
|
+
if (master) {
|
|
97
|
+
channel.set(master.name, master);
|
|
98
|
+
console.log(`[BMAD] Removed last expert, Master stays in channel ${channelId}`);
|
|
99
|
+
return {
|
|
100
|
+
removed: true,
|
|
101
|
+
fallback: true,
|
|
102
|
+
message: `Expert left. Master stays to keep the room open.`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Normal removal
|
|
108
|
+
const expert = channel.get(expertName);
|
|
109
|
+
channel.delete(expertName);
|
|
110
|
+
console.log(`[BMAD] Removed ${expert?.displayName || expertName} from channel ${channelId}`);
|
|
111
|
+
return { removed: true, fallback: false, message: 'Expert left the room' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Ensure Master is in the channel (add if not present)
|
|
116
|
+
* @param {string} channelId - Discord channel ID
|
|
117
|
+
* @returns {Object} Master expert object
|
|
118
|
+
*/
|
|
119
|
+
ensureMaster(channelId) {
|
|
120
|
+
const master = getDefaultExpert();
|
|
121
|
+
if (!master) {
|
|
122
|
+
console.warn('[BMAD] Default expert (Master) not found in manifest');
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.addParticipant(channelId, master);
|
|
127
|
+
return master;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Clear all participants from a channel
|
|
132
|
+
* @param {string} channelId - Discord channel ID
|
|
133
|
+
*/
|
|
134
|
+
clearChannel(channelId) {
|
|
135
|
+
this.channels.delete(channelId);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get channel count
|
|
140
|
+
* @returns {number} Number of active channels
|
|
141
|
+
*/
|
|
142
|
+
getActiveChannelCount() {
|
|
143
|
+
return this.channels.size;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Singleton instance
|
|
148
|
+
export const participantManager = new ParticipantManager();
|
package/src/bot.js
CHANGED
|
@@ -21,15 +21,26 @@
|
|
|
21
21
|
|
|
22
22
|
import { Client, GatewayIntentBits, Partials, Events, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
|
|
23
23
|
import { spawn, execSync } from 'child_process';
|
|
24
|
-
import { readFileSync, writeFileSync, existsSync, readdirSync, unlinkSync, mkdirSync } from 'fs';
|
|
24
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, unlinkSync, mkdirSync, chmodSync } from 'fs';
|
|
25
25
|
import { join } from 'path';
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
import { homedir } from 'os';
|
|
27
|
+
import { https } from 'https';
|
|
28
|
+
import { http } from 'http';
|
|
29
|
+
import { createWriteStream } from 'fs';
|
|
30
|
+
|
|
31
|
+
// Import performance monitoring utilities (local)
|
|
32
|
+
import {
|
|
33
|
+
createMonitor,
|
|
34
|
+
formatMonitorSummary
|
|
35
|
+
} from './core/perf-monitor.js';
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
syncMCPConfigLogged
|
|
39
|
+
} from './core/mcp-sync.js';
|
|
40
|
+
|
|
41
|
+
// Import centralized configuration (local, project-specific)
|
|
28
42
|
import config from './core/config.js';
|
|
29
43
|
|
|
30
|
-
// Import performance monitoring
|
|
31
|
-
import { createMonitor, formatMonitorSummary } from './core/perf-monitor.js';
|
|
32
|
-
|
|
33
44
|
// Extract commonly used config values for convenience
|
|
34
45
|
const BOT_TOKEN = config.discord.token;
|
|
35
46
|
const PROJECT_DIR = config.workspace.projectDir;
|
|
@@ -38,6 +49,7 @@ const ALLOWED_CHANNELS = config.discord.allowedChannels;
|
|
|
38
49
|
const CHANNEL_SESSIONS_FILE = config.paths.sessions;
|
|
39
50
|
const PENDING_DIR = config.paths.pending;
|
|
40
51
|
const HOOK_DIR = config.paths.hooks;
|
|
52
|
+
const USE_DOCKER = config.workspace.useDocker; // Whether to use Docker or run locally
|
|
41
53
|
const CONTAINER_NAME = config.workspace.containerName;
|
|
42
54
|
const DOCKER_IMAGE = config.workspace.dockerImage;
|
|
43
55
|
|
|
@@ -47,6 +59,8 @@ const CONTAINER_CONFIG_DIR = config.docker.containerPaths.configDir;
|
|
|
47
59
|
const CONTAINER_HOOK_DIR = config.docker.containerPaths.hookDir;
|
|
48
60
|
const CONTAINER_PENDING_DIR = config.docker.containerPaths.pendingDir;
|
|
49
61
|
const CONTAINER_SETTINGS_FILE = config.docker.containerPaths.settingsFile;
|
|
62
|
+
const CONTAINER_UPLOADS_DIR = config.docker.containerPaths.uploadsDir;
|
|
63
|
+
const UPLOADS_DIR = config.paths.uploads;
|
|
50
64
|
const PORT_MAPPINGS = config.workspace.portMappings;
|
|
51
65
|
const CLI_MAX_TURNS = config.cli.maxTurns;
|
|
52
66
|
const CLI_COMMAND = config.cli.command;
|
|
@@ -105,23 +119,231 @@ function updateHealthStatus(component, status) {
|
|
|
105
119
|
|
|
106
120
|
// Get overall health status
|
|
107
121
|
function getOverallHealth() {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
// Build health check string
|
|
123
|
+
const checks = [healthStatus.pm2 ? '✓' : '✗', healthStatus.discord ? '✓' : '✗'];
|
|
124
|
+
|
|
125
|
+
// Only check Docker if enabled
|
|
126
|
+
if (USE_DOCKER) {
|
|
127
|
+
checks.push(healthStatus.docker ? '✓' : '✗');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const healthString = checks.join(' ');
|
|
131
|
+
|
|
132
|
+
// Determine overall status
|
|
133
|
+
if (USE_DOCKER) {
|
|
134
|
+
// Docker mode: all components must be healthy
|
|
135
|
+
if (healthStatus.pm2 && healthStatus.discord && healthStatus.docker) {
|
|
136
|
+
return 'healthy';
|
|
137
|
+
} else if (healthStatus.pm2 && healthStatus.discord) {
|
|
138
|
+
return 'degraded'; // Running but Docker failed
|
|
139
|
+
} else if (healthStatus.pm2) {
|
|
140
|
+
return 'unhealthy'; // PM2 running but Discord disconnected
|
|
141
|
+
} else {
|
|
142
|
+
return 'failed';
|
|
143
|
+
}
|
|
120
144
|
} else {
|
|
121
|
-
|
|
145
|
+
// Non-Docker mode: only PM2 and Discord required
|
|
146
|
+
if (healthStatus.pm2 && healthStatus.discord) {
|
|
147
|
+
return 'healthy';
|
|
148
|
+
} else if (healthStatus.pm2) {
|
|
149
|
+
return 'unhealthy'; // PM2 running but Discord disconnected
|
|
150
|
+
} else {
|
|
151
|
+
return 'failed';
|
|
152
|
+
}
|
|
122
153
|
}
|
|
123
154
|
}
|
|
124
155
|
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// File Attachment Handling
|
|
158
|
+
// ============================================================================
|
|
159
|
+
|
|
160
|
+
// Supported file types for processing
|
|
161
|
+
const SUPPORTED_FILE_TYPES = {
|
|
162
|
+
images: ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'],
|
|
163
|
+
videos: ['.mp4', '.mov', '.webm', '.avi', '.mkv'],
|
|
164
|
+
documents: ['.pdf', '.doc', '.docx', '.txt', '.md', '.json', '.yaml', '.yml', '.xml', '.csv'],
|
|
165
|
+
code: ['.js', '.ts', '.py', '.java', '.c', '.cpp', '.h', '.go', '.rs', '.rb', '.php', '.sh', '.bat'],
|
|
166
|
+
archives: ['.zip', '.tar', '.gz', '.7z', '.rar'],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Download file from URL to local path
|
|
170
|
+
function downloadFile(url, destPath) {
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
const protocol = url.startsWith('https') ? https : http;
|
|
173
|
+
const file = createWriteStream(destPath);
|
|
174
|
+
|
|
175
|
+
protocol.get(url, {
|
|
176
|
+
headers: {
|
|
177
|
+
'User-Agent': 'DiscordBot (https://discord.js.org)'
|
|
178
|
+
}
|
|
179
|
+
}, (response) => {
|
|
180
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
181
|
+
// Follow redirect
|
|
182
|
+
downloadFile(response.headers.location, destPath)
|
|
183
|
+
.then(resolve)
|
|
184
|
+
.catch(reject);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (response.statusCode !== 200) {
|
|
189
|
+
reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
response.pipe(file);
|
|
194
|
+
|
|
195
|
+
file.on('finish', () => {
|
|
196
|
+
file.close();
|
|
197
|
+
resolve(destPath);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
file.on('error', (err) => {
|
|
201
|
+
unlinkSync(destPath);
|
|
202
|
+
reject(err);
|
|
203
|
+
});
|
|
204
|
+
}).on('error', (err) => {
|
|
205
|
+
unlinkSync(destPath);
|
|
206
|
+
reject(err);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Process Discord message attachments
|
|
212
|
+
async function processAttachments(message, channelId) {
|
|
213
|
+
if (!message.attachments || message.attachments.size === 0) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const uploadedFiles = [];
|
|
218
|
+
|
|
219
|
+
// Ensure uploads directory exists
|
|
220
|
+
const date = new Date();
|
|
221
|
+
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
222
|
+
const instanceUploadDir = join(UPLOADS_DIR, CONTAINER_NAME, dateStr, channelId);
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
mkdirSync(instanceUploadDir, { recursive: true });
|
|
226
|
+
} catch (e) {
|
|
227
|
+
console.error('[Upload] Failed to create upload directory:', e);
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
console.log(`[Upload] Processing ${message.attachments.size} attachment(s)`);
|
|
232
|
+
|
|
233
|
+
for (const [attachmentId, attachment] of message.attachments) {
|
|
234
|
+
try {
|
|
235
|
+
const url = attachment.url;
|
|
236
|
+
const filename = attachment.name;
|
|
237
|
+
const filesize = attachment.size;
|
|
238
|
+
|
|
239
|
+
console.log(`[Upload] Processing: ${filename} (${Math.round(filesize / 1024)}KB)`);
|
|
240
|
+
|
|
241
|
+
// Check file size (max 50MB)
|
|
242
|
+
if (filesize > 50 * 1024 * 1024) {
|
|
243
|
+
console.warn(`[Upload] Skipping ${filename}: file too large (${Math.round(filesize / 1024 / 1024)}MB)`);
|
|
244
|
+
await message.channel.send(`⚠️ File \`${filename}\` is too large (max 50MB)`);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Determine file type
|
|
249
|
+
const ext = filename.toLowerCase().substring(filename.lastIndexOf('.'));
|
|
250
|
+
const fileType = Object.entries(SUPPORTED_FILE_TYPES).find(([_, exts]) => exts.includes(ext));
|
|
251
|
+
|
|
252
|
+
if (!fileType) {
|
|
253
|
+
console.warn(`[Upload] Skipping ${filename}: unsupported file type`);
|
|
254
|
+
await message.channel.send(`⚠️ Unsupported file type: \`${filename}\``);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const [type, _] = fileType;
|
|
259
|
+
const destPath = join(instanceUploadDir, filename);
|
|
260
|
+
|
|
261
|
+
// Download file
|
|
262
|
+
console.log(`[Upload] Downloading ${url} to ${destPath}`);
|
|
263
|
+
await downloadFile(url, destPath);
|
|
264
|
+
|
|
265
|
+
// Make file readable
|
|
266
|
+
try {
|
|
267
|
+
chmodSync(destPath, 0o644);
|
|
268
|
+
} catch (e) {
|
|
269
|
+
// Ignore
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Calculate container path
|
|
273
|
+
const relativePath = destPath.replace(UPLOADS_DIR, '').replace(/^\//, '');
|
|
274
|
+
const containerPath = join(CONTAINER_UPLOADS_DIR, CONTAINER_NAME, dateStr, channelId, filename);
|
|
275
|
+
|
|
276
|
+
uploadedFiles.push({
|
|
277
|
+
hostPath: destPath,
|
|
278
|
+
containerPath: containerPath,
|
|
279
|
+
filename: filename,
|
|
280
|
+
type: type,
|
|
281
|
+
size: filesize,
|
|
282
|
+
url: url,
|
|
283
|
+
description: attachment.description || null,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
console.log(`[Upload] ✓ Saved: ${filename} (${type})`);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
console.error(`[Upload] Failed to process attachment:`, err);
|
|
289
|
+
await message.channel.send(`❌ Failed to download \`${attachment.name}\`: ${err.message}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return uploadedFiles;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Format file information for Claude prompt
|
|
297
|
+
function formatFilesForPrompt(uploadedFiles) {
|
|
298
|
+
if (uploadedFiles.length === 0) {
|
|
299
|
+
return '';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const lines = [
|
|
303
|
+
'',
|
|
304
|
+
'--- ATTACHED FILES ---',
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
uploadedFiles.forEach((file, idx) => {
|
|
308
|
+
const sizeKB = Math.round(file.size / 1024);
|
|
309
|
+
const sizeMB = (file.size / 1024 / 1024).toFixed(2);
|
|
310
|
+
|
|
311
|
+
let sizeStr = sizeKB > 1024 ? `${sizeMB}MB` : `${sizeKB}KB`;
|
|
312
|
+
|
|
313
|
+
lines.push(`${idx + 1}. **${file.filename}** (${file.type}, ${sizeStr})`);
|
|
314
|
+
lines.push(` - Container path: \`${file.containerPath}\``);
|
|
315
|
+
|
|
316
|
+
if (file.description) {
|
|
317
|
+
lines.push(` - Description: ${file.description}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Add specific instructions based on file type
|
|
321
|
+
if (file.type === 'images') {
|
|
322
|
+
lines.push(` - ⚠️ This is an image file. You can view it using the Read tool or analyze it with vision capabilities.`);
|
|
323
|
+
} else if (file.type === 'videos') {
|
|
324
|
+
lines.push(` - ⚠️ This is a video file. You may need to extract frames or use video analysis tools.`);
|
|
325
|
+
} else if (file.type === 'documents') {
|
|
326
|
+
lines.push(` - ⚠️ This is a document. You can read it with the Read tool for analysis.`);
|
|
327
|
+
} else if (file.type === 'code') {
|
|
328
|
+
lines.push(` - ⚠️ This is a code file. You can review and analyze it using Read and Grep tools.`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
lines.push('');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
lines.push('--- END OF ATTACHED FILES ---');
|
|
335
|
+
lines.push('');
|
|
336
|
+
|
|
337
|
+
return lines.join('\n');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Ensure uploads directory exists
|
|
341
|
+
try {
|
|
342
|
+
mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
343
|
+
} catch (e) {
|
|
344
|
+
console.error('[Upload] Failed to create uploads directory:', e);
|
|
345
|
+
}
|
|
346
|
+
|
|
125
347
|
// Ensure pending directory exists
|
|
126
348
|
try {
|
|
127
349
|
mkdirSync(PENDING_DIR, { recursive: true });
|
|
@@ -142,6 +364,13 @@ function isContainerRunning() {
|
|
|
142
364
|
|
|
143
365
|
// Start or ensure persistent container is running
|
|
144
366
|
function ensureContainer() {
|
|
367
|
+
// Skip Docker operations if not using Docker
|
|
368
|
+
if (!USE_DOCKER) {
|
|
369
|
+
console.log('[Docker] Docker is disabled (useDocker: false). Running in local mode.');
|
|
370
|
+
updateHealthStatus('docker', true); // Mark as "healthy" since we don't need Docker
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
|
|
145
374
|
// Log the paths being used
|
|
146
375
|
console.log('[Docker] PENDING_DIR (host):', PENDING_DIR);
|
|
147
376
|
console.log('[Docker] HOOK_DIR (host):', HOOK_DIR);
|
|
@@ -193,6 +422,7 @@ function ensureContainer() {
|
|
|
193
422
|
'-v', `${config.backend.configDir}:${CONTAINER_CONFIG_DIR}:rw`,
|
|
194
423
|
'-v', `${HOOK_DIR}:${CONTAINER_HOOK_DIR}:ro`,
|
|
195
424
|
'-v', `${PENDING_DIR}:${CONTAINER_PENDING_DIR}:rw`,
|
|
425
|
+
'-v', `${UPLOADS_DIR}:${CONTAINER_UPLOADS_DIR}:rw`,
|
|
196
426
|
`-e 'CLAUDE_CODE_OAUTH_TOKEN=${OAUTH_TOKEN || ''}'`,
|
|
197
427
|
envArgs,
|
|
198
428
|
'--entrypoint', 'tail',
|
|
@@ -202,6 +432,10 @@ function ensureContainer() {
|
|
|
202
432
|
|
|
203
433
|
execSync(dockerCmd);
|
|
204
434
|
console.log('[Docker] Container started successfully');
|
|
435
|
+
|
|
436
|
+
// Sync MCP configuration from host to container
|
|
437
|
+
syncMCPConfig();
|
|
438
|
+
|
|
205
439
|
updateHealthStatus('docker', true);
|
|
206
440
|
return true;
|
|
207
441
|
} catch (e) {
|
|
@@ -228,6 +462,11 @@ function ensureContainer() {
|
|
|
228
462
|
|
|
229
463
|
// Stop persistent container (call on bot shutdown)
|
|
230
464
|
function stopContainer() {
|
|
465
|
+
if (!USE_DOCKER) {
|
|
466
|
+
console.log('[Docker] Docker is disabled. No container to stop.');
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
231
470
|
try {
|
|
232
471
|
execSync(`docker stop ${CONTAINER_NAME} 2>/dev/null`);
|
|
233
472
|
execSync(`docker rm ${CONTAINER_NAME} 2>/dev/null`);
|
|
@@ -235,6 +474,11 @@ function stopContainer() {
|
|
|
235
474
|
} catch (e) {}
|
|
236
475
|
}
|
|
237
476
|
|
|
477
|
+
// Sync MCP configuration (wrapper for npm package function)
|
|
478
|
+
function syncMCPConfig() {
|
|
479
|
+
syncMCPConfigLogged(CONTAINER_NAME, PENDING_DIR, CONTAINER_PENDING_DIR, DOCKER_CHECK_TIMEOUT);
|
|
480
|
+
}
|
|
481
|
+
|
|
238
482
|
// Cleanup on exit
|
|
239
483
|
process.on('SIGINT', () => {
|
|
240
484
|
console.log('Shutting down...');
|
|
@@ -1011,13 +1255,19 @@ const commands = {
|
|
|
1011
1255
|
|
|
1012
1256
|
async claude(message, args) {
|
|
1013
1257
|
const userMessage = args.join(' ').trim();
|
|
1014
|
-
|
|
1258
|
+
const channelId = message.channel.id;
|
|
1259
|
+
|
|
1260
|
+
// Process attachments first
|
|
1261
|
+
const uploadedFiles = await processAttachments(message, channelId);
|
|
1262
|
+
|
|
1263
|
+
// If no text message but have attachments, provide a default message
|
|
1264
|
+
const actualMessage = userMessage || (uploadedFiles.length > 0 ? 'Please analyze the attached file(s)' : '');
|
|
1265
|
+
|
|
1266
|
+
if (!actualMessage) {
|
|
1015
1267
|
await commands.help(message);
|
|
1016
1268
|
return;
|
|
1017
1269
|
}
|
|
1018
1270
|
|
|
1019
|
-
const channelId = message.channel.id;
|
|
1020
|
-
|
|
1021
1271
|
// Check if there's an active task
|
|
1022
1272
|
if (activeTasks.has(channelId)) {
|
|
1023
1273
|
const task = activeTasks.get(channelId);
|
|
@@ -1078,7 +1328,7 @@ const commands = {
|
|
|
1078
1328
|
messageQueues.delete(channelId);
|
|
1079
1329
|
|
|
1080
1330
|
// Start new task
|
|
1081
|
-
await commands._processClaudeMessage(message,
|
|
1331
|
+
await commands._processClaudeMessage(message, actualMessage, channelId, uploadedFiles);
|
|
1082
1332
|
return;
|
|
1083
1333
|
}
|
|
1084
1334
|
|
|
@@ -1086,16 +1336,16 @@ const commands = {
|
|
|
1086
1336
|
if (!messageQueues.has(channelId)) {
|
|
1087
1337
|
messageQueues.set(channelId, []);
|
|
1088
1338
|
}
|
|
1089
|
-
messageQueues.get(channelId).push({ content:
|
|
1339
|
+
messageQueues.get(channelId).push({ content: actualMessage, timestamp: Date.now(), files: uploadedFiles });
|
|
1090
1340
|
const queueSize = messageQueues.get(channelId).length;
|
|
1091
1341
|
await message.channel.send(`📥 Queued (#${queueSize}) - will process after current task`);
|
|
1092
1342
|
return;
|
|
1093
1343
|
}
|
|
1094
1344
|
|
|
1095
|
-
await commands._processClaudeMessage(message,
|
|
1345
|
+
await commands._processClaudeMessage(message, actualMessage, channelId, uploadedFiles);
|
|
1096
1346
|
},
|
|
1097
1347
|
|
|
1098
|
-
async _processClaudeMessage(message, userMessage, channelId) {
|
|
1348
|
+
async _processClaudeMessage(message, userMessage, channelId, uploadedFiles = []) {
|
|
1099
1349
|
// Create stop button
|
|
1100
1350
|
const stopButton = new ActionRowBuilder()
|
|
1101
1351
|
.addComponents(
|
|
@@ -1131,6 +1381,13 @@ const commands = {
|
|
|
1131
1381
|
// Parse flags
|
|
1132
1382
|
let actualMessage = userMessage;
|
|
1133
1383
|
|
|
1384
|
+
// Add file information to the message if there are attachments
|
|
1385
|
+
if (uploadedFiles.length > 0) {
|
|
1386
|
+
const fileInfo = formatFilesForPrompt(uploadedFiles);
|
|
1387
|
+
actualMessage = fileInfo + userMessage;
|
|
1388
|
+
console.log(`[Claude] Added ${uploadedFiles.length} file(s) to prompt`);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1134
1391
|
// Base CLI args with PreToolUse hook for permission control
|
|
1135
1392
|
// Hook handles: auto-approve safe tools, request Discord approval for risky tools
|
|
1136
1393
|
const cliArgs = [
|
|
@@ -1139,6 +1396,7 @@ const commands = {
|
|
|
1139
1396
|
'--verbose',
|
|
1140
1397
|
'--max-turns', String(CLI_MAX_TURNS),
|
|
1141
1398
|
'--settings', CONTAINER_SETTINGS_FILE,
|
|
1399
|
+
'--model', config.cli.model, // Add model selection
|
|
1142
1400
|
];
|
|
1143
1401
|
|
|
1144
1402
|
// Add model selection (defaults to opus)
|
|
@@ -1217,21 +1475,42 @@ const commands = {
|
|
|
1217
1475
|
let lastStatusUpdate = 0;
|
|
1218
1476
|
const task = activeTasks.get(channelId);
|
|
1219
1477
|
|
|
1220
|
-
//
|
|
1221
|
-
|
|
1222
|
-
'exec', '-i',
|
|
1223
|
-
CONTAINER_NAME,
|
|
1224
|
-
CLI_COMMAND,
|
|
1225
|
-
...cliArgs
|
|
1226
|
-
];
|
|
1478
|
+
// Execute CLI command (either in Docker or locally based on configuration)
|
|
1479
|
+
let child;
|
|
1227
1480
|
|
|
1228
|
-
|
|
1481
|
+
if (USE_DOCKER) {
|
|
1482
|
+
// Use docker exec to run CLI in the persistent container
|
|
1483
|
+
const dockerArgs = [
|
|
1484
|
+
'exec', '-i',
|
|
1485
|
+
CONTAINER_NAME,
|
|
1486
|
+
CLI_COMMAND,
|
|
1487
|
+
...cliArgs
|
|
1488
|
+
];
|
|
1229
1489
|
|
|
1230
|
-
|
|
1231
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1232
|
-
});
|
|
1490
|
+
console.log('[Docker] Executing in container:', CONTAINER_NAME);
|
|
1233
1491
|
|
|
1234
|
-
|
|
1492
|
+
child = spawn('docker', dockerArgs, {
|
|
1493
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
child.stdin.end();
|
|
1497
|
+
} else {
|
|
1498
|
+
// Run CLI directly on local host
|
|
1499
|
+
console.log('[Local] Executing locally:', CLI_COMMAND, ...cliArgs);
|
|
1500
|
+
|
|
1501
|
+
child = spawn(CLI_COMMAND, cliArgs, {
|
|
1502
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1503
|
+
cwd: PROJECT_DIR,
|
|
1504
|
+
env: {
|
|
1505
|
+
...process.env,
|
|
1506
|
+
CLAUDE_CODE_OAUTH_TOKEN: OAUTH_TOKEN || '',
|
|
1507
|
+
HOME: process.env.HOME,
|
|
1508
|
+
PATH: process.env.PATH
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
child.stdin.end();
|
|
1513
|
+
}
|
|
1235
1514
|
|
|
1236
1515
|
// Update status periodically (with stop button)
|
|
1237
1516
|
// forceUpdate=true bypasses throttle for important state changes
|
|
@@ -1616,9 +1895,14 @@ client.on(Events.ClientReady, async () => {
|
|
|
1616
1895
|
// Send startup notification to Discord channels
|
|
1617
1896
|
try {
|
|
1618
1897
|
const uptime = Math.floor((Date.now() - healthStatus.startTime) / 1000);
|
|
1619
|
-
const dockerStatus = healthStatus.docker ? '✓ Connected' : '✗ Not Available';
|
|
1620
1898
|
const overallStatus = overallHealth.toUpperCase();
|
|
1621
1899
|
|
|
1900
|
+
// Build execution mode info
|
|
1901
|
+
const execMode = USE_DOCKER ? 'Docker Container' : 'Local Host';
|
|
1902
|
+
const dockerStatus = USE_DOCKER
|
|
1903
|
+
? (healthStatus.docker ? '✓ Connected' : '✗ Not Available')
|
|
1904
|
+
: '⊘ Disabled (Local Mode)';
|
|
1905
|
+
|
|
1622
1906
|
// Build startup message
|
|
1623
1907
|
const startupMessage = {
|
|
1624
1908
|
embeds: [{
|
|
@@ -1630,6 +1914,11 @@ client.on(Events.ClientReady, async () => {
|
|
|
1630
1914
|
value: overallStatus,
|
|
1631
1915
|
inline: true
|
|
1632
1916
|
},
|
|
1917
|
+
{
|
|
1918
|
+
name: 'Execution Mode',
|
|
1919
|
+
value: execMode,
|
|
1920
|
+
inline: true
|
|
1921
|
+
},
|
|
1633
1922
|
{
|
|
1634
1923
|
name: 'Docker',
|
|
1635
1924
|
value: dockerStatus,
|
|
@@ -1642,7 +1931,9 @@ client.on(Events.ClientReady, async () => {
|
|
|
1642
1931
|
},
|
|
1643
1932
|
{
|
|
1644
1933
|
name: 'Components',
|
|
1645
|
-
value:
|
|
1934
|
+
value: USE_DOCKER
|
|
1935
|
+
? `PM2: ✓\nDiscord: ✓\nDocker: ${healthStatus.docker ? '✓' : '✗'}`
|
|
1936
|
+
: `PM2: ✓\nDiscord: ✓\nDocker: ⊘ (Local Mode)`,
|
|
1646
1937
|
inline: false
|
|
1647
1938
|
}
|
|
1648
1939
|
],
|