opencode-autognosis 1.0.1 → 2.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/README.md +346 -76
- package/dist/activeset.d.ts +3 -0
- package/dist/activeset.js +498 -0
- package/dist/chunk-cards.d.ts +3 -0
- package/dist/chunk-cards.js +586 -0
- package/dist/git-worktree.d.ts +3 -0
- package/dist/git-worktree.js +384 -0
- package/dist/index.js +12 -0
- package/dist/module-summaries.d.ts +3 -0
- package/dist/module-summaries.js +510 -0
- package/dist/performance-optimization.d.ts +3 -0
- package/dist/performance-optimization.js +653 -0
- package/dist/testing-infrastructure.d.ts +3 -0
- package/dist/testing-infrastructure.js +603 -0
- package/package.json +27 -3
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { exec } from "node:child_process";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as fsSync from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import * as crypto from "node:crypto";
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const PROJECT_ROOT = process.cwd();
|
|
10
|
+
const OPENCODE_DIR = path.join(PROJECT_ROOT, ".opencode");
|
|
11
|
+
const CACHE_DIR = path.join(OPENCODE_DIR, "cache");
|
|
12
|
+
const PERF_DIR = path.join(OPENCODE_DIR, "performance");
|
|
13
|
+
const METRICS_DIR = path.join(OPENCODE_DIR, "metrics");
|
|
14
|
+
// Internal logging
|
|
15
|
+
function log(message, data) {
|
|
16
|
+
console.error(`[Performance] ${message}`, data || '');
|
|
17
|
+
}
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// HELPERS
|
|
20
|
+
// =============================================================================
|
|
21
|
+
async function runCmd(cmd, cwd = PROJECT_ROOT, timeoutMs = 30000) {
|
|
22
|
+
try {
|
|
23
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
24
|
+
cwd,
|
|
25
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
26
|
+
timeout: timeoutMs
|
|
27
|
+
});
|
|
28
|
+
return { stdout: stdout.trim(), stderr: stderr.trim() };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.signal === 'SIGTERM' && error.code === undefined) {
|
|
32
|
+
return { stdout: "", stderr: `Command timed out after ${timeoutMs}ms`, error, timedOut: true };
|
|
33
|
+
}
|
|
34
|
+
return { stdout: "", stderr: error.message, error };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function ensurePerfDirs() {
|
|
38
|
+
await fs.mkdir(CACHE_DIR, { recursive: true });
|
|
39
|
+
await fs.mkdir(PERF_DIR, { recursive: true });
|
|
40
|
+
await fs.mkdir(METRICS_DIR, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
function generateCacheKey(operation, params) {
|
|
43
|
+
const paramHash = crypto.createHash('md5').update(JSON.stringify(params)).digest('hex');
|
|
44
|
+
return `${operation}-${paramHash}`;
|
|
45
|
+
}
|
|
46
|
+
async function getCacheEntry(key) {
|
|
47
|
+
try {
|
|
48
|
+
const cachePath = path.join(CACHE_DIR, `${key}.json`);
|
|
49
|
+
if (!fsSync.existsSync(cachePath)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const entry = JSON.parse(await fs.readFile(cachePath, 'utf-8'));
|
|
53
|
+
// Check TTL if present
|
|
54
|
+
if (entry.metadata.ttl_seconds) {
|
|
55
|
+
const age = (Date.now() - new Date(entry.metadata.created_at).getTime()) / 1000;
|
|
56
|
+
if (age > entry.metadata.ttl_seconds) {
|
|
57
|
+
await fs.unlink(cachePath);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Update access metadata
|
|
62
|
+
entry.metadata.last_accessed = new Date().toISOString();
|
|
63
|
+
entry.metadata.access_count++;
|
|
64
|
+
await fs.writeFile(cachePath, JSON.stringify(entry, null, 2));
|
|
65
|
+
return entry;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function setCacheEntry(key, value, ttlSeconds) {
|
|
72
|
+
try {
|
|
73
|
+
const entry = {
|
|
74
|
+
key,
|
|
75
|
+
value,
|
|
76
|
+
metadata: {
|
|
77
|
+
created_at: new Date().toISOString(),
|
|
78
|
+
last_accessed: new Date().toISOString(),
|
|
79
|
+
access_count: 1,
|
|
80
|
+
size_bytes: JSON.stringify(value).length,
|
|
81
|
+
ttl_seconds: ttlSeconds
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const cachePath = path.join(CACHE_DIR, `${key}.json`);
|
|
85
|
+
await fs.writeFile(cachePath, JSON.stringify(entry, null, 2));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
// Fail silently for cache errors
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function recordMetrics(metrics) {
|
|
92
|
+
try {
|
|
93
|
+
const metricsPath = path.join(METRICS_DIR, `metrics-${Date.now()}.json`);
|
|
94
|
+
await fs.writeFile(metricsPath, JSON.stringify(metrics, null, 2));
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
// Fail silently for metrics errors
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function measureMemoryUsage() {
|
|
101
|
+
if (typeof process !== 'undefined' && process.memoryUsage) {
|
|
102
|
+
return process.memoryUsage().heapUsed / 1024 / 1024; // MB
|
|
103
|
+
}
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
// =============================================================================
|
|
107
|
+
// PERFORMANCE OPTIMIZATION TOOLS
|
|
108
|
+
// =============================================================================
|
|
109
|
+
export function performanceTools() {
|
|
110
|
+
return {
|
|
111
|
+
perf_incremental_index: tool({
|
|
112
|
+
description: "Perform incremental re-indexing of the codebase. Only processes changed files since last index.",
|
|
113
|
+
args: {
|
|
114
|
+
force_full: tool.schema.boolean().optional().default(false).describe("Force full re-indexing instead of incremental"),
|
|
115
|
+
parallel_workers: tool.schema.number().optional().default(4).describe("Number of parallel workers for indexing"),
|
|
116
|
+
background: tool.schema.boolean().optional().default(false).describe("Run indexing in background")
|
|
117
|
+
},
|
|
118
|
+
async execute({ force_full, parallel_workers, background }) {
|
|
119
|
+
log("Tool call: perf_incremental_index", { force_full, parallel_workers, background });
|
|
120
|
+
const startTime = Date.now();
|
|
121
|
+
const metrics = {
|
|
122
|
+
operation: "incremental_index",
|
|
123
|
+
start_time: startTime,
|
|
124
|
+
end_time: 0,
|
|
125
|
+
duration_ms: 0,
|
|
126
|
+
memory_usage_mb: measureMemoryUsage(),
|
|
127
|
+
cache_hits: 0,
|
|
128
|
+
cache_misses: 0,
|
|
129
|
+
success: false
|
|
130
|
+
};
|
|
131
|
+
try {
|
|
132
|
+
await ensurePerfDirs();
|
|
133
|
+
// Load indexing state
|
|
134
|
+
const statePath = path.join(PERF_DIR, "indexing-state.json");
|
|
135
|
+
let indexingState = {
|
|
136
|
+
last_indexed: "1970-01-01T00:00:00.000Z",
|
|
137
|
+
files_processed: 0,
|
|
138
|
+
files_indexed: 0,
|
|
139
|
+
indexing_duration_ms: 0,
|
|
140
|
+
cache_status: "stale"
|
|
141
|
+
};
|
|
142
|
+
if (!force_full && fsSync.existsSync(statePath)) {
|
|
143
|
+
indexingState = JSON.parse(await fs.readFile(statePath, 'utf-8'));
|
|
144
|
+
}
|
|
145
|
+
if (background) {
|
|
146
|
+
// Create background task
|
|
147
|
+
const taskId = `task-index-${Date.now()}`;
|
|
148
|
+
const taskPath = path.join(PERF_DIR, `${taskId}.json`);
|
|
149
|
+
const backgroundTask = {
|
|
150
|
+
id: taskId,
|
|
151
|
+
type: "indexing",
|
|
152
|
+
status: "pending",
|
|
153
|
+
progress: 0,
|
|
154
|
+
metadata: {
|
|
155
|
+
force_full,
|
|
156
|
+
parallel_workers,
|
|
157
|
+
started_at: new Date().toISOString()
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
await fs.writeFile(taskPath, JSON.stringify(backgroundTask, null, 2));
|
|
161
|
+
// Start background indexing (simplified for demo)
|
|
162
|
+
setTimeout(() => runBackgroundIndexing(taskId, indexingState), 100);
|
|
163
|
+
return JSON.stringify({
|
|
164
|
+
status: "BACKGROUND_STARTED",
|
|
165
|
+
task_id: taskId,
|
|
166
|
+
message: "Incremental indexing started in background"
|
|
167
|
+
}, null, 2);
|
|
168
|
+
}
|
|
169
|
+
// Get changed files
|
|
170
|
+
const { stdout: gitStatus } = await runCmd("git status --porcelain");
|
|
171
|
+
const { stdout: gitDiff } = await runCmd(`git diff --name-only --since="${indexingState.last_indexed}"`);
|
|
172
|
+
const changedFiles = gitDiff.split('\n').filter(Boolean);
|
|
173
|
+
const allFiles = await getAllSourceFiles();
|
|
174
|
+
const filesToIndex = force_full ? allFiles : changedFiles.length > 0 ? changedFiles : allFiles.slice(0, 10); // Fallback to some files
|
|
175
|
+
// Process files
|
|
176
|
+
let filesProcessed = 0;
|
|
177
|
+
let filesIndexed = 0;
|
|
178
|
+
for (const file of filesToIndex) {
|
|
179
|
+
try {
|
|
180
|
+
filesProcessed++;
|
|
181
|
+
// Check if file needs indexing
|
|
182
|
+
const filePath = path.join(PROJECT_ROOT, file);
|
|
183
|
+
if (!fsSync.existsSync(filePath))
|
|
184
|
+
continue;
|
|
185
|
+
const stats = await fs.stat(filePath);
|
|
186
|
+
const lastModified = stats.mtime.toISOString();
|
|
187
|
+
if (!force_full && lastModified < indexingState.last_indexed) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
// Index the file (simplified - would create chunk cards, etc.)
|
|
191
|
+
await indexFile(filePath);
|
|
192
|
+
filesIndexed++;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
// Continue with other files
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Update indexing state
|
|
199
|
+
indexingState.last_indexed = new Date().toISOString();
|
|
200
|
+
indexingState.files_processed += filesProcessed;
|
|
201
|
+
indexingState.files_indexed += filesIndexed;
|
|
202
|
+
indexingState.indexing_duration_ms = Date.now() - startTime;
|
|
203
|
+
indexingState.cache_status = "fresh";
|
|
204
|
+
await fs.writeFile(statePath, JSON.stringify(indexingState, null, 2));
|
|
205
|
+
// Update metrics
|
|
206
|
+
metrics.end_time = Date.now();
|
|
207
|
+
metrics.duration_ms = metrics.end_time - metrics.start_time;
|
|
208
|
+
metrics.success = true;
|
|
209
|
+
await recordMetrics(metrics);
|
|
210
|
+
return JSON.stringify({
|
|
211
|
+
status: "SUCCESS",
|
|
212
|
+
indexing: {
|
|
213
|
+
mode: force_full ? "full" : "incremental",
|
|
214
|
+
files_processed: filesProcessed,
|
|
215
|
+
files_indexed: filesIndexed,
|
|
216
|
+
duration_ms: Date.now() - startTime,
|
|
217
|
+
cache_status: indexingState.cache_status
|
|
218
|
+
},
|
|
219
|
+
state: indexingState
|
|
220
|
+
}, null, 2);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
metrics.end_time = Date.now();
|
|
224
|
+
metrics.duration_ms = metrics.end_time - metrics.start_time;
|
|
225
|
+
metrics.success = false;
|
|
226
|
+
metrics.error = error instanceof Error ? error.message : `${error}`;
|
|
227
|
+
await recordMetrics(metrics);
|
|
228
|
+
return JSON.stringify({
|
|
229
|
+
status: "ERROR",
|
|
230
|
+
message: error instanceof Error ? error.message : `${error}`
|
|
231
|
+
}, null, 2);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}),
|
|
235
|
+
perf_cache_get: tool({
|
|
236
|
+
description: "Retrieve a value from the performance cache with automatic hit/miss tracking.",
|
|
237
|
+
args: {
|
|
238
|
+
operation: tool.schema.string().describe("Operation type for cache key"),
|
|
239
|
+
params: tool.schema.any().describe("Parameters for cache key generation")
|
|
240
|
+
},
|
|
241
|
+
async execute({ operation, params }) {
|
|
242
|
+
log("Tool call: perf_cache_get", { operation, params });
|
|
243
|
+
try {
|
|
244
|
+
const cacheKey = generateCacheKey(operation, params);
|
|
245
|
+
const entry = await getCacheEntry(cacheKey);
|
|
246
|
+
if (entry) {
|
|
247
|
+
return JSON.stringify({
|
|
248
|
+
status: "HIT",
|
|
249
|
+
cache_key: cacheKey,
|
|
250
|
+
value: entry.value,
|
|
251
|
+
metadata: entry.metadata
|
|
252
|
+
}, null, 2);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
return JSON.stringify({
|
|
256
|
+
status: "MISS",
|
|
257
|
+
cache_key: cacheKey,
|
|
258
|
+
message: "Cache entry not found"
|
|
259
|
+
}, null, 2);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
return JSON.stringify({
|
|
264
|
+
status: "ERROR",
|
|
265
|
+
message: error instanceof Error ? error.message : `${error}`
|
|
266
|
+
}, null, 2);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}),
|
|
270
|
+
perf_cache_set: tool({
|
|
271
|
+
description: "Store a value in the performance cache with optional TTL.",
|
|
272
|
+
args: {
|
|
273
|
+
operation: tool.schema.string().describe("Operation type for cache key"),
|
|
274
|
+
params: tool.schema.any().describe("Parameters for cache key generation"),
|
|
275
|
+
value: tool.schema.any().describe("Value to cache"),
|
|
276
|
+
ttl_seconds: tool.schema.number().optional().describe("Time-to-live in seconds")
|
|
277
|
+
},
|
|
278
|
+
async execute({ operation, params, value, ttl_seconds }) {
|
|
279
|
+
log("Tool call: perf_cache_set", { operation, params, ttl_seconds });
|
|
280
|
+
try {
|
|
281
|
+
const cacheKey = generateCacheKey(operation, params);
|
|
282
|
+
await setCacheEntry(cacheKey, value, ttl_seconds);
|
|
283
|
+
return JSON.stringify({
|
|
284
|
+
status: "SUCCESS",
|
|
285
|
+
cache_key: cacheKey,
|
|
286
|
+
ttl_seconds,
|
|
287
|
+
message: "Value cached successfully"
|
|
288
|
+
}, null, 2);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
return JSON.stringify({
|
|
292
|
+
status: "ERROR",
|
|
293
|
+
message: error instanceof Error ? error.message : `${error}`
|
|
294
|
+
}, null, 2);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}),
|
|
298
|
+
perf_cache_cleanup: tool({
|
|
299
|
+
description: "Clean up expired and stale cache entries to free memory.",
|
|
300
|
+
args: {
|
|
301
|
+
max_age_hours: tool.schema.number().optional().default(24).describe("Maximum age for cache entries"),
|
|
302
|
+
max_size_mb: tool.schema.number().optional().default(100).describe("Maximum cache size in MB"),
|
|
303
|
+
dry_run: tool.schema.boolean().optional().default(false).describe("Show what would be deleted without actually deleting")
|
|
304
|
+
},
|
|
305
|
+
async execute({ max_age_hours, max_size_mb, dry_run }) {
|
|
306
|
+
log("Tool call: perf_cache_cleanup", { max_age_hours, max_size_mb, dry_run });
|
|
307
|
+
try {
|
|
308
|
+
await ensurePerfDirs();
|
|
309
|
+
const files = await fs.readdir(CACHE_DIR);
|
|
310
|
+
const now = Date.now();
|
|
311
|
+
const maxAge = max_age_hours * 60 * 60 * 1000;
|
|
312
|
+
let totalSize = 0;
|
|
313
|
+
let expiredCount = 0;
|
|
314
|
+
let oversizedCount = 0;
|
|
315
|
+
const deletedFiles = [];
|
|
316
|
+
for (const file of files) {
|
|
317
|
+
if (file.endsWith('.json')) {
|
|
318
|
+
const filePath = path.join(CACHE_DIR, file);
|
|
319
|
+
const stats = await fs.stat(filePath);
|
|
320
|
+
const age = now - stats.mtime.getTime();
|
|
321
|
+
try {
|
|
322
|
+
const entry = JSON.parse(await fs.readFile(filePath, 'utf-8'));
|
|
323
|
+
totalSize += entry.metadata.size_bytes || 0;
|
|
324
|
+
const shouldDelete = age > maxAge || totalSize > (max_size_mb * 1024 * 1024);
|
|
325
|
+
if (shouldDelete) {
|
|
326
|
+
if (age > maxAge)
|
|
327
|
+
expiredCount++;
|
|
328
|
+
else
|
|
329
|
+
oversizedCount++;
|
|
330
|
+
deletedFiles.push({
|
|
331
|
+
file,
|
|
332
|
+
reason: age > maxAge ? "expired" : "oversized",
|
|
333
|
+
size_bytes: entry.metadata.size_bytes || 0,
|
|
334
|
+
age_hours: age / (1000 * 60 * 60)
|
|
335
|
+
});
|
|
336
|
+
if (!dry_run) {
|
|
337
|
+
await fs.unlink(filePath);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
// Remove corrupted files
|
|
343
|
+
if (!dry_run) {
|
|
344
|
+
await fs.unlink(filePath);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return JSON.stringify({
|
|
350
|
+
status: "SUCCESS",
|
|
351
|
+
cleanup: {
|
|
352
|
+
dry_run,
|
|
353
|
+
max_age_hours,
|
|
354
|
+
max_size_mb,
|
|
355
|
+
total_files: files.length,
|
|
356
|
+
expired_files: expiredCount,
|
|
357
|
+
oversized_files: oversizedCount,
|
|
358
|
+
deleted_files: deletedFiles,
|
|
359
|
+
total_size_mb: Math.round(totalSize / 1024 / 1024 * 100) / 100
|
|
360
|
+
}
|
|
361
|
+
}, null, 2);
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
return JSON.stringify({
|
|
365
|
+
status: "ERROR",
|
|
366
|
+
message: error instanceof Error ? error.message : `${error}`
|
|
367
|
+
}, null, 2);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}),
|
|
371
|
+
perf_metrics_get: tool({
|
|
372
|
+
description: "Retrieve performance metrics for analysis and monitoring.",
|
|
373
|
+
args: {
|
|
374
|
+
operation_filter: tool.schema.string().optional().describe("Filter by operation type (regex)"),
|
|
375
|
+
time_range_hours: tool.schema.number().optional().default(24).describe("Time range in hours"),
|
|
376
|
+
limit: tool.schema.number().optional().default(100).describe("Maximum number of metrics to return")
|
|
377
|
+
},
|
|
378
|
+
async execute({ operation_filter, time_range_hours, limit }) {
|
|
379
|
+
log("Tool call: perf_metrics_get", { operation_filter, time_range_hours, limit });
|
|
380
|
+
try {
|
|
381
|
+
await ensurePerfDirs();
|
|
382
|
+
const files = await fs.readdir(METRICS_DIR);
|
|
383
|
+
const cutoffTime = Date.now() - (time_range_hours * 60 * 60 * 1000);
|
|
384
|
+
const metrics = [];
|
|
385
|
+
for (const file of files) {
|
|
386
|
+
if (file.endsWith('.json')) {
|
|
387
|
+
const filePath = path.join(METRICS_DIR, file);
|
|
388
|
+
const stats = await fs.stat(filePath);
|
|
389
|
+
if (stats.mtime.getTime() > cutoffTime) {
|
|
390
|
+
try {
|
|
391
|
+
const metric = JSON.parse(await fs.readFile(filePath, 'utf-8'));
|
|
392
|
+
// Apply operation filter if specified
|
|
393
|
+
if (operation_filter && !new RegExp(operation_filter).test(metric.operation)) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
metrics.push(metric);
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
// Skip corrupted files
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// Sort by start time (most recent first)
|
|
405
|
+
metrics.sort((a, b) => b.start_time - a.start_time);
|
|
406
|
+
// Apply limit
|
|
407
|
+
const limitedMetrics = metrics.slice(0, limit);
|
|
408
|
+
// Calculate summary statistics
|
|
409
|
+
const summary = calculateMetricsSummary(limitedMetrics);
|
|
410
|
+
return JSON.stringify({
|
|
411
|
+
status: "SUCCESS",
|
|
412
|
+
metrics: limitedMetrics,
|
|
413
|
+
summary,
|
|
414
|
+
filters: {
|
|
415
|
+
operation_filter,
|
|
416
|
+
time_range_hours,
|
|
417
|
+
limit
|
|
418
|
+
}
|
|
419
|
+
}, null, 2);
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
return JSON.stringify({
|
|
423
|
+
status: "ERROR",
|
|
424
|
+
message: error instanceof Error ? error.message : `${error}`
|
|
425
|
+
}, null, 2);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}),
|
|
429
|
+
perf_background_status: tool({
|
|
430
|
+
description: "Check status of background tasks and operations.",
|
|
431
|
+
args: {
|
|
432
|
+
task_id: tool.schema.string().optional().describe("Specific task ID to check"),
|
|
433
|
+
task_type: tool.schema.enum(["indexing", "caching", "cleanup", "analysis"]).optional().describe("Filter by task type")
|
|
434
|
+
},
|
|
435
|
+
async execute({ task_id, task_type }) {
|
|
436
|
+
log("Tool call: perf_background_status", { task_id, task_type });
|
|
437
|
+
try {
|
|
438
|
+
await ensurePerfDirs();
|
|
439
|
+
const files = await fs.readdir(PERF_DIR);
|
|
440
|
+
const tasks = [];
|
|
441
|
+
for (const file of files) {
|
|
442
|
+
if (file.startsWith('task-') && file.endsWith('.json')) {
|
|
443
|
+
try {
|
|
444
|
+
const taskPath = path.join(PERF_DIR, file);
|
|
445
|
+
const task = JSON.parse(await fs.readFile(taskPath, 'utf-8'));
|
|
446
|
+
// Apply filters
|
|
447
|
+
if (task_id && task.id !== task_id)
|
|
448
|
+
continue;
|
|
449
|
+
if (task_type && task.type !== task_type)
|
|
450
|
+
continue;
|
|
451
|
+
tasks.push(task);
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
// Skip corrupted files
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return JSON.stringify({
|
|
459
|
+
status: "SUCCESS",
|
|
460
|
+
tasks,
|
|
461
|
+
total_count: tasks.length,
|
|
462
|
+
filters: {
|
|
463
|
+
task_id,
|
|
464
|
+
task_type
|
|
465
|
+
}
|
|
466
|
+
}, null, 2);
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
return JSON.stringify({
|
|
470
|
+
status: "ERROR",
|
|
471
|
+
message: error instanceof Error ? error.message : `${error}`
|
|
472
|
+
}, null, 2);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}),
|
|
476
|
+
perf_optimize_memory: tool({
|
|
477
|
+
description: "Optimize memory usage for large codebases with intelligent caching and cleanup.",
|
|
478
|
+
args: {
|
|
479
|
+
aggressive: tool.schema.boolean().optional().default(false).describe("Use aggressive memory optimization"),
|
|
480
|
+
target_memory_mb: tool.schema.number().optional().default(500).describe("Target memory usage in MB"),
|
|
481
|
+
preserve_recent: tool.schema.boolean().optional().default(true).describe("Preserve recently accessed cache entries")
|
|
482
|
+
},
|
|
483
|
+
async execute({ aggressive, target_memory_mb, preserve_recent }) {
|
|
484
|
+
log("Tool call: perf_optimize_memory", { aggressive, target_memory_mb, preserve_recent });
|
|
485
|
+
try {
|
|
486
|
+
await ensurePerfDirs();
|
|
487
|
+
const currentMemory = measureMemoryUsage();
|
|
488
|
+
const memoryReductionNeeded = Math.max(0, currentMemory - target_memory_mb);
|
|
489
|
+
if (memoryReductionNeeded <= 0) {
|
|
490
|
+
return JSON.stringify({
|
|
491
|
+
status: "SUCCESS",
|
|
492
|
+
message: "Memory usage already within target",
|
|
493
|
+
current_memory_mb: currentMemory,
|
|
494
|
+
target_memory_mb
|
|
495
|
+
}, null, 2);
|
|
496
|
+
}
|
|
497
|
+
// Get all cache entries
|
|
498
|
+
const cacheFiles = await fs.readdir(CACHE_DIR);
|
|
499
|
+
const cacheEntries = [];
|
|
500
|
+
for (const file of cacheFiles) {
|
|
501
|
+
if (file.endsWith('.json')) {
|
|
502
|
+
try {
|
|
503
|
+
const filePath = path.join(CACHE_DIR, file);
|
|
504
|
+
const entry = JSON.parse(await fs.readFile(filePath, 'utf-8'));
|
|
505
|
+
cacheEntries.push({
|
|
506
|
+
file,
|
|
507
|
+
entry,
|
|
508
|
+
size_bytes: entry.metadata.size_bytes || 0,
|
|
509
|
+
last_accessed: new Date(entry.metadata.last_accessed).getTime(),
|
|
510
|
+
access_count: entry.metadata.access_count || 0
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
// Remove corrupted files
|
|
515
|
+
await fs.unlink(path.join(CACHE_DIR, file));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// Sort by priority (keep recently accessed and frequently used)
|
|
520
|
+
cacheEntries.sort((a, b) => {
|
|
521
|
+
const scoreA = (preserve_recent ? a.last_accessed : 0) + (a.access_count * 1000);
|
|
522
|
+
const scoreB = (preserve_recent ? b.last_accessed : 0) + (b.access_count * 1000);
|
|
523
|
+
return scoreB - scoreA;
|
|
524
|
+
});
|
|
525
|
+
// Remove entries until target is met
|
|
526
|
+
let removedSize = 0;
|
|
527
|
+
let removedCount = 0;
|
|
528
|
+
const targetRemovalSize = memoryReductionNeeded * 1024 * 1024; // Convert to bytes
|
|
529
|
+
for (let i = cacheEntries.length - 1; i >= 0; i--) {
|
|
530
|
+
if (removedSize >= targetRemovalSize)
|
|
531
|
+
break;
|
|
532
|
+
const entry = cacheEntries[i];
|
|
533
|
+
await fs.unlink(path.join(CACHE_DIR, entry.file));
|
|
534
|
+
removedSize += entry.size_bytes;
|
|
535
|
+
removedCount++;
|
|
536
|
+
}
|
|
537
|
+
// Force garbage collection if available
|
|
538
|
+
if (global.gc) {
|
|
539
|
+
global.gc();
|
|
540
|
+
}
|
|
541
|
+
const finalMemory = measureMemoryUsage();
|
|
542
|
+
return JSON.stringify({
|
|
543
|
+
status: "SUCCESS",
|
|
544
|
+
optimization: {
|
|
545
|
+
aggressive,
|
|
546
|
+
target_memory_mb,
|
|
547
|
+
preserve_recent,
|
|
548
|
+
initial_memory_mb: currentMemory,
|
|
549
|
+
final_memory_mb: finalMemory,
|
|
550
|
+
memory_freed_mb: Math.round((currentMemory - finalMemory) * 100) / 100,
|
|
551
|
+
cache_entries_removed: removedCount,
|
|
552
|
+
cache_size_freed_mb: Math.round(removedSize / 1024 / 1024 * 100) / 100
|
|
553
|
+
}
|
|
554
|
+
}, null, 2);
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
return JSON.stringify({
|
|
558
|
+
status: "ERROR",
|
|
559
|
+
message: error instanceof Error ? error.message : `${error}`
|
|
560
|
+
}, null, 2);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
})
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
// =============================================================================
|
|
567
|
+
// HELPER FUNCTIONS
|
|
568
|
+
// =============================================================================
|
|
569
|
+
async function getAllSourceFiles() {
|
|
570
|
+
const extensions = ['.ts', '.js', '.tsx', '.jsx', '.py', '.go', '.rs', '.cpp', '.c'];
|
|
571
|
+
const sourceFiles = [];
|
|
572
|
+
async function scanDirectory(dir) {
|
|
573
|
+
try {
|
|
574
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
575
|
+
for (const entry of entries) {
|
|
576
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const fullPath = path.join(dir, entry.name);
|
|
580
|
+
if (entry.isDirectory()) {
|
|
581
|
+
await scanDirectory(fullPath);
|
|
582
|
+
}
|
|
583
|
+
else if (extensions.includes(path.extname(entry.name))) {
|
|
584
|
+
sourceFiles.push(path.relative(PROJECT_ROOT, fullPath));
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
catch (error) {
|
|
589
|
+
// Skip directories we can't read
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
await scanDirectory(PROJECT_ROOT);
|
|
593
|
+
return sourceFiles;
|
|
594
|
+
}
|
|
595
|
+
async function indexFile(filePath) {
|
|
596
|
+
// Simplified indexing - would create chunk cards, analyze symbols, etc.
|
|
597
|
+
// For now, just touch the file to update its timestamp
|
|
598
|
+
const stats = await fs.stat(filePath);
|
|
599
|
+
// Indexing logic would go here
|
|
600
|
+
}
|
|
601
|
+
async function runBackgroundIndexing(taskId, indexingState) {
|
|
602
|
+
try {
|
|
603
|
+
const taskPath = path.join(PERF_DIR, `${taskId}.json`);
|
|
604
|
+
let task = JSON.parse(await fs.readFile(taskPath, 'utf-8'));
|
|
605
|
+
// Update task status
|
|
606
|
+
task.status = "running";
|
|
607
|
+
task.started_at = new Date().toISOString();
|
|
608
|
+
await fs.writeFile(taskPath, JSON.stringify(task, null, 2));
|
|
609
|
+
// Simulate background indexing work
|
|
610
|
+
for (let i = 0; i < 10; i++) {
|
|
611
|
+
task.progress = (i + 1) * 10;
|
|
612
|
+
await fs.writeFile(taskPath, JSON.stringify(task, null, 2));
|
|
613
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
614
|
+
}
|
|
615
|
+
// Complete task
|
|
616
|
+
task.status = "completed";
|
|
617
|
+
task.completed_at = new Date().toISOString();
|
|
618
|
+
task.progress = 100;
|
|
619
|
+
await fs.writeFile(taskPath, JSON.stringify(task, null, 2));
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
// Update task with error
|
|
623
|
+
const taskPath = path.join(PERF_DIR, `${taskId}.json`);
|
|
624
|
+
const task = JSON.parse(await fs.readFile(taskPath, 'utf-8'));
|
|
625
|
+
task.status = "failed";
|
|
626
|
+
task.error = error instanceof Error ? error.message : `${error}`;
|
|
627
|
+
task.completed_at = new Date().toISOString();
|
|
628
|
+
await fs.writeFile(taskPath, JSON.stringify(task, null, 2));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function calculateMetricsSummary(metrics) {
|
|
632
|
+
if (metrics.length === 0) {
|
|
633
|
+
return {
|
|
634
|
+
total_operations: 0,
|
|
635
|
+
success_rate: 0,
|
|
636
|
+
avg_duration_ms: 0,
|
|
637
|
+
avg_memory_mb: 0
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
const successCount = metrics.filter(m => m.success).length;
|
|
641
|
+
const totalDuration = metrics.reduce((sum, m) => sum + m.duration_ms, 0);
|
|
642
|
+
const totalMemory = metrics.reduce((sum, m) => sum + m.memory_usage_mb, 0);
|
|
643
|
+
return {
|
|
644
|
+
total_operations: metrics.length,
|
|
645
|
+
success_rate: Math.round((successCount / metrics.length) * 100),
|
|
646
|
+
avg_duration_ms: Math.round(totalDuration / metrics.length),
|
|
647
|
+
avg_memory_mb: Math.round(totalMemory / metrics.length * 100) / 100,
|
|
648
|
+
operations_by_type: metrics.reduce((acc, m) => {
|
|
649
|
+
acc[m.operation] = (acc[m.operation] || 0) + 1;
|
|
650
|
+
return acc;
|
|
651
|
+
}, {})
|
|
652
|
+
};
|
|
653
|
+
}
|