agent-window 1.0.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.
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Configuration Module
3
+ *
4
+ * Centralizes all configuration loading and validation.
5
+ * Supports both config file and environment variables.
6
+ */
7
+
8
+ import { readFileSync, existsSync } from 'fs';
9
+ import { dirname, join } from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const PROJECT_ROOT = join(__dirname, '../..');
14
+
15
+ /**
16
+ * Expand tilde (~) in paths to home directory
17
+ */
18
+ function expandPath(path) {
19
+ if (!path) return path;
20
+ if (path.startsWith('~/')) {
21
+ return join(process.env.HOME, path.slice(2));
22
+ }
23
+ if (path === '~') {
24
+ return process.env.HOME;
25
+ }
26
+ return path;
27
+ }
28
+
29
+ /**
30
+ * Parse color value (supports hex string "0x...", "#...", or number)
31
+ */
32
+ function parseColor(value, defaultColor) {
33
+ if (value === undefined || value === null) return defaultColor;
34
+ if (typeof value === 'number') return value;
35
+ if (typeof value === 'string') {
36
+ // Handle "0x..." format
37
+ if (value.startsWith('0x') || value.startsWith('0X')) {
38
+ return parseInt(value, 16);
39
+ }
40
+ // Handle "#..." format
41
+ if (value.startsWith('#')) {
42
+ return parseInt(value.slice(1), 16);
43
+ }
44
+ // Try parsing as decimal
45
+ const num = parseInt(value, 10);
46
+ if (!isNaN(num)) return num;
47
+ }
48
+ return defaultColor;
49
+ }
50
+
51
+ /**
52
+ * Load and validate configuration
53
+ */
54
+ function loadConfig() {
55
+ // Support CONFIG_PATH environment variable for multi-bot setups
56
+ const configPath = process.env.CONFIG_PATH || process.env.AGENT_BRIDGE_CONFIG || join(PROJECT_ROOT, 'config', 'config.json');
57
+ let fileConfig = {};
58
+
59
+ if (existsSync(configPath)) {
60
+ try {
61
+ fileConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
62
+ } catch (e) {
63
+ console.error('[Config] Error parsing config.json:', e.message);
64
+ }
65
+ }
66
+
67
+ // Build configuration with defaults
68
+ const config = {
69
+ // === Discord Configuration ===
70
+ discord: {
71
+ token: fileConfig.BOT_TOKEN || process.env.DISCORD_BOT_TOKEN || '',
72
+ allowedChannels: fileConfig.ALLOWED_CHANNELS
73
+ ? fileConfig.ALLOWED_CHANNELS.split(',').map(id => id.trim())
74
+ : null,
75
+ },
76
+
77
+ // === Backend Configuration ===
78
+ backend: {
79
+ type: fileConfig.backend?.type || 'claude-code',
80
+ oauthToken: fileConfig.CLAUDE_CODE_OAUTH_TOKEN || process.env.CLAUDE_CODE_OAUTH_TOKEN || '',
81
+ apiKey: fileConfig.backend?.apiKey || process.env.ANTHROPIC_API_KEY || '',
82
+ configDir: expandPath(fileConfig.backend?.configDir) || `${process.env.HOME}/.claude`,
83
+ },
84
+
85
+ // === Workspace Configuration ===
86
+ workspace: {
87
+ projectDir: expandPath(fileConfig.PROJECT_DIR) || process.cwd(),
88
+ containerName: fileConfig.workspace?.containerName || 'claude-discord-bot',
89
+ dockerImage: fileConfig.workspace?.dockerImage || 'claude-sandbox',
90
+ portMappings: fileConfig.workspace?.portMappings || ['5173:5173'],
91
+ },
92
+
93
+ // === Paths (derived from PROJECT_ROOT) ===
94
+ paths: {
95
+ root: PROJECT_ROOT,
96
+ config: join(PROJECT_ROOT, 'config'),
97
+ hooks: join(PROJECT_ROOT, 'hooks'),
98
+ pending: join(PROJECT_ROOT, '.pending-permissions'),
99
+ sessions: join(PROJECT_ROOT, '.discord-channel-sessions.json'),
100
+ },
101
+
102
+ // === CLI Configuration ===
103
+ cli: {
104
+ command: fileConfig.cli?.command || 'claude',
105
+ maxTurns: fileConfig.cli?.maxTurns || 50,
106
+ outputFormat: 'stream-json',
107
+ verbose: true,
108
+ taskTimeout: fileConfig.cli?.taskTimeout || 3600000, // 1 hour default
109
+ },
110
+
111
+ // === Permission Configuration ===
112
+ permissions: {
113
+ hookEnabled: fileConfig.permissions?.hookEnabled !== false,
114
+ pollInterval: fileConfig.permissions?.pollInterval || 300,
115
+ timeout: fileConfig.permissions?.timeout || 120000,
116
+ },
117
+
118
+ // === Docker Configuration ===
119
+ docker: {
120
+ checkTimeout: fileConfig.docker?.checkTimeout || 5000, // 5 seconds default
121
+ // Container internal paths (usually don't need to change)
122
+ containerPaths: {
123
+ workspace: fileConfig.docker?.containerPaths?.workspace || '/workspace',
124
+ configDir: fileConfig.docker?.containerPaths?.configDir || '/home/claude/.claude',
125
+ hookDir: fileConfig.docker?.containerPaths?.hookDir || '/permission-hook',
126
+ pendingDir: fileConfig.docker?.containerPaths?.pendingDir || '/pending-permissions',
127
+ settingsFile: fileConfig.docker?.containerPaths?.settingsFile || '/permission-hook/settings.json',
128
+ },
129
+ // Environment variables passed to container
130
+ env: {
131
+ TERM: 'dumb',
132
+ CI: 'true',
133
+ NO_COLOR: '1',
134
+ FORCE_COLOR: '0',
135
+ HOOK_PENDING_DIR: fileConfig.docker?.containerPaths?.pendingDir || '/pending-permissions',
136
+ },
137
+ },
138
+
139
+ // === UI Configuration ===
140
+ ui: {
141
+ statusUpdateThrottle: fileConfig.ui?.statusUpdateThrottle || 500, // ms
142
+ theme: {
143
+ success: parseColor(fileConfig.ui?.theme?.success, 0x9ece6a),
144
+ error: parseColor(fileConfig.ui?.theme?.error, 0xf7768e),
145
+ info: parseColor(fileConfig.ui?.theme?.info, 0x7aa2f7),
146
+ warning: parseColor(fileConfig.ui?.theme?.warning, 0xf0ad4e),
147
+ },
148
+ },
149
+
150
+ // === Platform Configuration ===
151
+ platform: {
152
+ type: fileConfig.platform?.type || 'discord',
153
+ messageMaxLength: fileConfig.platform?.messageMaxLength || 1900,
154
+ retryDelay: fileConfig.platform?.retryDelay || 500,
155
+ previewLengths: {
156
+ command: fileConfig.platform?.previewLengths?.command || 500,
157
+ fileContent: fileConfig.platform?.previewLengths?.fileContent || 300,
158
+ editPreview: fileConfig.platform?.previewLengths?.editPreview || 150,
159
+ jsonInput: fileConfig.platform?.previewLengths?.jsonInput || 400,
160
+ response: fileConfig.platform?.previewLengths?.response || 200,
161
+ },
162
+ },
163
+ };
164
+
165
+ return config;
166
+ }
167
+
168
+ /**
169
+ * Validate required configuration
170
+ */
171
+ function validateConfig(config) {
172
+ const errors = [];
173
+
174
+ if (!config.discord.token) {
175
+ errors.push('BOT_TOKEN is required (set in config.json or DISCORD_BOT_TOKEN env)');
176
+ }
177
+
178
+ if (!config.workspace.projectDir) {
179
+ errors.push('PROJECT_DIR is required');
180
+ }
181
+
182
+ if (errors.length > 0) {
183
+ console.error('[Config] Validation errors:');
184
+ errors.forEach(err => console.error(` - ${err}`));
185
+ process.exit(1);
186
+ }
187
+
188
+ return config;
189
+ }
190
+
191
+ // Load and export config
192
+ const config = validateConfig(loadConfig());
193
+
194
+ export default config;
195
+ export { PROJECT_ROOT, loadConfig, validateConfig };
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Performance Monitor Module
3
+ *
4
+ * Provides lightweight performance monitoring and detailed logging
5
+ * for AgentBridge bot operations.
6
+ */
7
+
8
+ import { writeFileSync, existsSync, mkdirSync } from 'fs';
9
+ import { join } from 'path';
10
+
11
+ /**
12
+ * Performance Monitor Class
13
+ * Tracks metrics during bot operation
14
+ */
15
+ export class PerformanceMonitor {
16
+ constructor(channelId, options = {}) {
17
+ this.channelId = channelId;
18
+ this.enabled = options.enabled !== false;
19
+ this.logFile = options.logFile;
20
+ this.verbose = options.verbose || false;
21
+
22
+ // Metrics
23
+ this.startTime = Date.now();
24
+ this.metrics = {
25
+ // Tool call counts
26
+ toolCalls: {
27
+ Read: 0,
28
+ Write: 0,
29
+ Edit: 0,
30
+ Bash: 0,
31
+ Task: 0,
32
+ WebFetch: 0,
33
+ WebSearch: 0,
34
+ NotebookEdit: 0,
35
+ Other: 0,
36
+ },
37
+ // Subagent tracking
38
+ subagentDepth: 0,
39
+ maxSubagentDepth: 0,
40
+ subagentCalls: 0,
41
+ // Timing
42
+ firstToolCall: null,
43
+ lastToolCall: null,
44
+ permissionWaits: [],
45
+ // Bottleneck detection
46
+ slowOperations: [], // { operation, duration, timestamp }
47
+ };
48
+
49
+ // Detailed log entries (for analysis)
50
+ this.logEntries = [];
51
+ }
52
+
53
+ /**
54
+ * Record a tool call
55
+ */
56
+ recordToolCall(toolName, input = {}) {
57
+ if (!this.enabled) return;
58
+
59
+ const normalizedTool = this._normalizeToolName(toolName);
60
+ if (this.metrics.toolCalls[normalizedTool] !== undefined) {
61
+ this.metrics.toolCalls[normalizedTool]++;
62
+ } else {
63
+ this.metrics.toolCalls.Other++;
64
+ }
65
+
66
+ const now = Date.now();
67
+ if (!this.metrics.firstToolCall) {
68
+ this.metrics.firstToolCall = now;
69
+ }
70
+ this.metrics.lastToolCall = now;
71
+
72
+ // Track Task (subagent) depth
73
+ if (normalizedTool === 'Task') {
74
+ this.metrics.subagentDepth++;
75
+ this.metrics.subagentCalls++;
76
+ if (this.metrics.subagentDepth > this.metrics.maxSubagentDepth) {
77
+ this.metrics.maxSubagentDepth = this.metrics.subagentDepth;
78
+ }
79
+ }
80
+
81
+ // Log detailed entry
82
+ this._log('tool_use', {
83
+ tool: normalizedTool,
84
+ input: this._sanitizeInput(input),
85
+ depth: normalizedTool === 'Task' ? this.metrics.subagentDepth : undefined,
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Record a tool result
91
+ */
92
+ recordToolResult(toolName, duration = null) {
93
+ if (!this.enabled) return;
94
+
95
+ const normalizedTool = this._normalizeToolName(toolName);
96
+
97
+ // Track Task completion (decrease depth)
98
+ if (normalizedTool === 'Task') {
99
+ this.metrics.subagentDepth = Math.max(0, this.metrics.subagentDepth - 1);
100
+ }
101
+
102
+ // Track slow operations
103
+ if (duration && duration > 1000) {
104
+ this.metrics.slowOperations.push({
105
+ operation: normalizedTool,
106
+ duration,
107
+ timestamp: Date.now(),
108
+ });
109
+ }
110
+
111
+ // Log detailed entry
112
+ this._log('tool_result', {
113
+ tool: normalizedTool,
114
+ duration,
115
+ depth: this.metrics.subagentDepth,
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Record permission wait time
121
+ */
122
+ recordPermissionWait(duration) {
123
+ if (!this.enabled) return;
124
+
125
+ this.metrics.permissionWaits.push({
126
+ duration,
127
+ timestamp: Date.now(),
128
+ });
129
+
130
+ this._log('permission_wait', { duration });
131
+ }
132
+
133
+ /**
134
+ * Get performance summary for display
135
+ */
136
+ getSummary() {
137
+ if (!this.enabled) return '';
138
+
139
+ const elapsed = Date.now() - this.startTime;
140
+ const totalTools = Object.values(this.metrics.toolCalls).reduce((a, b) => a + b, 0);
141
+ const elapsedSec = Math.floor(elapsed / 1000);
142
+ const elapsedStr = elapsedSec < 60
143
+ ? `${elapsedSec}s`
144
+ : `${Math.floor(elapsedSec / 60)}m${elapsedSec % 60}s`;
145
+
146
+ // Build summary line
147
+ const parts = [];
148
+
149
+ // Elapsed time - always show if > 0
150
+ if (elapsedSec > 0) {
151
+ parts.push(`⏱️ ${elapsedStr}`);
152
+ }
153
+
154
+ // Tool calls - always show
155
+ parts.push(`🔧 ${totalTools}`);
156
+
157
+ // Subagents - show if any
158
+ if (this.metrics.subagentCalls > 0) {
159
+ parts.push(`🤖 ${this.metrics.subagentCalls}`);
160
+ }
161
+
162
+ // Permission waits
163
+ if (this.metrics.permissionWaits.length > 0) {
164
+ const totalWait = this.metrics.permissionWaits.reduce((a, b) => a + b.duration, 0);
165
+ parts.push(`🔒 ${Math.round(totalWait / 1000)}s`);
166
+ }
167
+
168
+ // Bottleneck warning
169
+ if (this.metrics.slowOperations.length > 0) {
170
+ const avgSlow = this.metrics.slowOperations.reduce((a, b) => a + b.duration, 0)
171
+ / this.metrics.slowOperations.length;
172
+ if (avgSlow > 3000) {
173
+ parts.push(`⚠️ Slow ops`);
174
+ }
175
+ }
176
+
177
+ return parts.join(' | ');
178
+ }
179
+
180
+ /**
181
+ * Get detailed metrics object
182
+ */
183
+ getMetrics() {
184
+ return {
185
+ ...this.metrics,
186
+ elapsed: Date.now() - this.startTime,
187
+ totalToolCalls: Object.values(this.metrics.toolCalls).reduce((a, b) => a + b, 0),
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Save detailed logs to file
193
+ */
194
+ async saveLogs(outputDir) {
195
+ if (!this.enabled || this.logEntries.length === 0) return;
196
+
197
+ try {
198
+ if (!existsSync(outputDir)) {
199
+ mkdirSync(outputDir, { recursive: true });
200
+ }
201
+
202
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
203
+ const filename = `perf-${this.channelId}-${timestamp}.jsonl`;
204
+ const filepath = join(outputDir, filename);
205
+
206
+ const lines = this.logEntries.map(entry => JSON.stringify(entry)).join('\n');
207
+ writeFileSync(filepath, lines);
208
+
209
+ return filepath;
210
+ } catch (e) {
211
+ console.error('[Perf] Failed to save logs:', e.message);
212
+ return null;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Get bottleneck analysis
218
+ */
219
+ analyzeBottlenecks() {
220
+ const issues = [];
221
+
222
+ // Check for excessive tool calls
223
+ const totalTools = Object.values(this.metrics.toolCalls).reduce((a, b) => a + b, 0);
224
+ if (totalTools > 50) {
225
+ issues.push({
226
+ type: 'excessive_tools',
227
+ severity: 'warning',
228
+ message: `Excessive tool calls: ${totalTools}`,
229
+ suggestion: 'Consider consolidating operations',
230
+ });
231
+ }
232
+
233
+ // Check for deep subagent nesting
234
+ if (this.metrics.maxSubagentDepth > 3) {
235
+ issues.push({
236
+ type: 'deep_nesting',
237
+ severity: 'warning',
238
+ message: `Deep subagent nesting: ${this.metrics.maxSubagentDepth} levels`,
239
+ suggestion: 'Subagent nesting may cause delays',
240
+ });
241
+ }
242
+
243
+ // Check for slow operations
244
+ if (this.metrics.slowOperations.length > 0) {
245
+ const slowByTool = {};
246
+ for (const op of this.metrics.slowOperations) {
247
+ if (!slowByTool[op.operation]) {
248
+ slowByTool[op.operation] = { count: 0, totalDuration: 0 };
249
+ }
250
+ slowByTool[op.operation].count++;
251
+ slowByTool[op.operation].totalDuration += op.duration;
252
+ }
253
+
254
+ for (const [tool, data] of Object.entries(slowByTool)) {
255
+ const avg = data.totalDuration / data.count;
256
+ if (avg > 3000) {
257
+ issues.push({
258
+ type: 'slow_tool',
259
+ severity: 'warning',
260
+ message: `${tool} averaging ${Math.round(avg / 1000)}s per call`,
261
+ suggestion: tool === 'Bash' ? 'Commands may be slow' : 'API/IO may be bottleneck',
262
+ });
263
+ }
264
+ }
265
+ }
266
+
267
+ // Check for permission wait times
268
+ if (this.metrics.permissionWaits.length > 0) {
269
+ const totalWait = this.metrics.permissionWaits.reduce((a, b) => a + b.duration, 0);
270
+ const avgWait = totalWait / this.metrics.permissionWaits.length;
271
+ if (avgWait > 5000) {
272
+ issues.push({
273
+ type: 'slow_permissions',
274
+ severity: 'info',
275
+ message: `Average permission wait: ${Math.round(avgWait / 1000)}s`,
276
+ suggestion: 'Consider auto-approving safe operations',
277
+ });
278
+ }
279
+ }
280
+
281
+ return issues;
282
+ }
283
+
284
+ /**
285
+ * Normalize tool name
286
+ */
287
+ _normalizeToolName(name) {
288
+ const map = {
289
+ 'read': 'Read',
290
+ 'write': 'Write',
291
+ 'edit': 'Edit',
292
+ 'bash': 'Bash',
293
+ 'task': 'Task',
294
+ 'webfetch': 'WebFetch',
295
+ 'websearch': 'WebSearch',
296
+ 'notebookedit': 'NotebookEdit',
297
+ };
298
+ return map[name.toLowerCase()] || name;
299
+ }
300
+
301
+ /**
302
+ * Sanitize input for logging
303
+ */
304
+ _sanitizeInput(input) {
305
+ if (!input) return undefined;
306
+
307
+ const sanitized = {};
308
+ for (const [key, value] of Object.entries(input)) {
309
+ if (key === 'content') {
310
+ // Truncate content
311
+ sanitized[key] = typeof value === 'string'
312
+ ? value.substring(0, 100) + (value.length > 100 ? '...' : '')
313
+ : value;
314
+ } else if (key === 'file_path') {
315
+ // Just show filename
316
+ sanitized[key] = value?.split('/').pop();
317
+ } else {
318
+ sanitized[key] = value;
319
+ }
320
+ }
321
+ return sanitized;
322
+ }
323
+
324
+ /**
325
+ * Add log entry
326
+ */
327
+ _log(type, data) {
328
+ if (!this.verbose && !this.logFile) return;
329
+
330
+ const entry = {
331
+ timestamp: new Date().toISOString(),
332
+ type,
333
+ channelId: this.channelId,
334
+ elapsed: Date.now() - this.startTime,
335
+ ...data,
336
+ };
337
+
338
+ this.logEntries.push(entry);
339
+
340
+ // Also log to console in verbose mode
341
+ if (this.verbose) {
342
+ console.log(`[PERF] ${type}`, JSON.stringify(data).substring(0, 200));
343
+ }
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Create a monitor instance for a channel
349
+ */
350
+ export function createMonitor(channelId, options) {
351
+ return new PerformanceMonitor(channelId, options);
352
+ }
353
+
354
+ /**
355
+ * Get summary string for display in status
356
+ */
357
+ export function formatMonitorSummary(monitor) {
358
+ if (!monitor || !monitor.enabled) return '';
359
+ return monitor.getSummary();
360
+ }