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.
- package/Dockerfile +23 -0
- package/README.md +138 -0
- package/SECURITY.md +31 -0
- package/bin/cli.js +743 -0
- package/config/config.example.json +70 -0
- package/docker-compose.yml +31 -0
- package/docs/legacy/DEVELOPMENT.md +174 -0
- package/docs/legacy/HANDOVER.md +149 -0
- package/ecosystem.config.cjs +26 -0
- package/hooks/hook.mjs +299 -0
- package/hooks/settings.json +15 -0
- package/package.json +45 -0
- package/sandbox/Dockerfile +61 -0
- package/scripts/install.sh +114 -0
- package/src/bot.js +1518 -0
- package/src/core/config.js +195 -0
- package/src/core/perf-monitor.js +360 -0
|
@@ -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
|
+
}
|