@gotza02/sequential-thinking 10000.0.6 → 10000.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codestore.d.ts +1 -1
- package/dist/graph.d.ts +9 -1
- package/dist/graph.js +89 -18
- package/dist/http-server.js +116 -8
- package/dist/lib.d.ts +3 -1
- package/dist/lib.js +40 -4
- package/dist/notes.d.ts +1 -1
- package/dist/tools/coding.js +43 -1
- package/dist/tools/web.d.ts +1 -1
- package/dist/tools/web.js +120 -47
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +64 -1
- package/package.json +1 -1
package/dist/codestore.d.ts
CHANGED
package/dist/graph.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare class ProjectKnowledgeGraph {
|
|
|
11
11
|
private cachePath;
|
|
12
12
|
private configResolver;
|
|
13
13
|
private configHash;
|
|
14
|
+
private MAX_CACHE_SIZE;
|
|
14
15
|
private getConcurrencyLimit;
|
|
15
16
|
constructor();
|
|
16
17
|
/**
|
|
@@ -22,7 +23,7 @@ export declare class ProjectKnowledgeGraph {
|
|
|
22
23
|
cachedFiles: number;
|
|
23
24
|
parsedFiles: number;
|
|
24
25
|
}>;
|
|
25
|
-
build(rootDir: string): Promise<{
|
|
26
|
+
build(rootDir: string, onProgress?: (progress: any) => void): Promise<{
|
|
26
27
|
nodeCount: number;
|
|
27
28
|
totalFiles: number;
|
|
28
29
|
cachedFiles: number;
|
|
@@ -30,6 +31,13 @@ export declare class ProjectKnowledgeGraph {
|
|
|
30
31
|
}>;
|
|
31
32
|
private loadCache;
|
|
32
33
|
private saveCache;
|
|
34
|
+
clearCache(): Promise<void>;
|
|
35
|
+
getCacheStats(): {
|
|
36
|
+
fileCount: number;
|
|
37
|
+
totalSize: string;
|
|
38
|
+
configHash: string | undefined;
|
|
39
|
+
version: string;
|
|
40
|
+
};
|
|
33
41
|
private getAllFiles;
|
|
34
42
|
private isMonorepoRoot;
|
|
35
43
|
private parseFile;
|
package/dist/graph.js
CHANGED
|
@@ -184,6 +184,7 @@ export class ProjectKnowledgeGraph {
|
|
|
184
184
|
cachePath = '';
|
|
185
185
|
configResolver = null;
|
|
186
186
|
configHash = '';
|
|
187
|
+
MAX_CACHE_SIZE = 1000;
|
|
187
188
|
// Dynamic concurrency based on file count
|
|
188
189
|
getConcurrencyLimit(fileCount) {
|
|
189
190
|
if (fileCount < 100)
|
|
@@ -214,9 +215,22 @@ export class ProjectKnowledgeGraph {
|
|
|
214
215
|
this.configResolver = null;
|
|
215
216
|
return await this.build(rootDir);
|
|
216
217
|
}
|
|
217
|
-
async build(rootDir) {
|
|
218
|
+
async build(rootDir, onProgress) {
|
|
218
219
|
try {
|
|
220
|
+
// Validate input
|
|
221
|
+
if (!rootDir || typeof rootDir !== 'string') {
|
|
222
|
+
throw new Error('rootDir must be a non-empty string');
|
|
223
|
+
}
|
|
224
|
+
if (rootDir.length > 1000) {
|
|
225
|
+
throw new Error('rootDir path too long');
|
|
226
|
+
}
|
|
219
227
|
this.rootDir = path.resolve(rootDir);
|
|
228
|
+
// Security check: prevent path traversal
|
|
229
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
230
|
+
const cwd = process.cwd();
|
|
231
|
+
if (!resolvedRoot.startsWith(cwd) && !process.env.ALLOW_EXTERNAL_PATHS) {
|
|
232
|
+
throw new Error('Access denied: Cannot build graph outside current working directory');
|
|
233
|
+
}
|
|
220
234
|
this.cachePath = path.join(this.rootDir, '.gemini_graph_cache.json');
|
|
221
235
|
// Check if rootDir exists and is a directory
|
|
222
236
|
const stats = await fs.stat(this.rootDir);
|
|
@@ -262,6 +276,8 @@ export class ProjectKnowledgeGraph {
|
|
|
262
276
|
}
|
|
263
277
|
// Step 3: Parse new/modified files concurrently with dynamic limit
|
|
264
278
|
const CONCURRENCY_LIMIT = this.getConcurrencyLimit(filesToParse.length);
|
|
279
|
+
let processedFiles = files.length - filesToParse.length; // Start with cached files count
|
|
280
|
+
const totalFiles = files.length;
|
|
265
281
|
for (let i = 0; i < filesToParse.length; i += CONCURRENCY_LIMIT) {
|
|
266
282
|
const chunk = filesToParse.slice(i, i + CONCURRENCY_LIMIT);
|
|
267
283
|
await Promise.all(chunk.map(async (file) => {
|
|
@@ -274,6 +290,14 @@ export class ProjectKnowledgeGraph {
|
|
|
274
290
|
imports: data.imports,
|
|
275
291
|
symbols: data.symbols
|
|
276
292
|
};
|
|
293
|
+
processedFiles++;
|
|
294
|
+
if (onProgress && processedFiles % 10 === 0) {
|
|
295
|
+
onProgress({
|
|
296
|
+
processed: processedFiles,
|
|
297
|
+
total: totalFiles,
|
|
298
|
+
percentage: Math.round((processedFiles / totalFiles) * 100)
|
|
299
|
+
});
|
|
300
|
+
}
|
|
277
301
|
}));
|
|
278
302
|
}
|
|
279
303
|
// Update config hash in cache
|
|
@@ -317,6 +341,20 @@ export class ProjectKnowledgeGraph {
|
|
|
317
341
|
}
|
|
318
342
|
async saveCache() {
|
|
319
343
|
try {
|
|
344
|
+
// Limit cache size
|
|
345
|
+
const fileEntries = Object.entries(this.cache.files);
|
|
346
|
+
if (fileEntries.length > this.MAX_CACHE_SIZE) {
|
|
347
|
+
// Sort by access time (if available) or keep most recent
|
|
348
|
+
// Since we only track mtime, we can't strictly purge by access time unless we add atime.
|
|
349
|
+
// We'll use mtime as a proxy for relevance or just keep the ones we encountered (if sorted by something else).
|
|
350
|
+
// Actually, let's just keep the ones with latest mtime? No, that might purge old stable files.
|
|
351
|
+
// The patch suggests: sort by mtime.
|
|
352
|
+
const sorted = fileEntries.sort((a, b) => {
|
|
353
|
+
return (b[1].mtime || 0) - (a[1].mtime || 0);
|
|
354
|
+
});
|
|
355
|
+
this.cache.files = Object.fromEntries(sorted.slice(0, this.MAX_CACHE_SIZE));
|
|
356
|
+
console.warn(`[Graph] Cache trimmed to ${this.MAX_CACHE_SIZE} entries`);
|
|
357
|
+
}
|
|
320
358
|
const tmpPath = this.cachePath + '.tmp';
|
|
321
359
|
await fs.writeFile(tmpPath, JSON.stringify(this.cache, null, 2), 'utf-8');
|
|
322
360
|
await fs.rename(tmpPath, this.cachePath);
|
|
@@ -325,6 +363,26 @@ export class ProjectKnowledgeGraph {
|
|
|
325
363
|
console.error('Failed to save graph cache:', e);
|
|
326
364
|
}
|
|
327
365
|
}
|
|
366
|
+
async clearCache() {
|
|
367
|
+
this.cache = { version: '2.0', files: {} };
|
|
368
|
+
try {
|
|
369
|
+
await fs.unlink(this.cachePath);
|
|
370
|
+
console.log('[Graph] Cache cleared');
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
// Ignore if file doesn't exist
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
getCacheStats() {
|
|
377
|
+
const files = Object.keys(this.cache.files);
|
|
378
|
+
const totalSize = JSON.stringify(this.cache).length;
|
|
379
|
+
return {
|
|
380
|
+
fileCount: files.length,
|
|
381
|
+
totalSize: `${Math.round(totalSize / 1024)}KB`,
|
|
382
|
+
configHash: this.cache.configHash,
|
|
383
|
+
version: this.cache.version
|
|
384
|
+
};
|
|
385
|
+
}
|
|
328
386
|
async getAllFiles(dir) {
|
|
329
387
|
try {
|
|
330
388
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
@@ -387,24 +445,37 @@ export class ProjectKnowledgeGraph {
|
|
|
387
445
|
return false;
|
|
388
446
|
}
|
|
389
447
|
async parseFile(filePath) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
448
|
+
try {
|
|
449
|
+
const ext = path.extname(filePath);
|
|
450
|
+
// Check file size before parsing
|
|
451
|
+
const stats = await fs.stat(filePath);
|
|
452
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
453
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
454
|
+
console.warn(`[Graph] Skipping large file: ${filePath} (${Math.round(stats.size / 1024 / 1024)}MB)`);
|
|
455
|
+
return { imports: [], symbols: [] };
|
|
456
|
+
}
|
|
457
|
+
if (['.ts', '.js', '.tsx', '.jsx', '.mjs', '.cjs'].includes(ext)) {
|
|
458
|
+
return await this.parseTypeScript(filePath);
|
|
459
|
+
}
|
|
460
|
+
else if (ext === '.py') {
|
|
461
|
+
return await this.parsePython(filePath);
|
|
462
|
+
}
|
|
463
|
+
else if (ext === '.go') {
|
|
464
|
+
return await this.parseGo(filePath);
|
|
465
|
+
}
|
|
466
|
+
else if (ext === '.rs') {
|
|
467
|
+
return await this.parseRust(filePath);
|
|
468
|
+
}
|
|
469
|
+
else if (['.java', '.kt', '.kts'].includes(ext)) {
|
|
470
|
+
return await this.parseJavaLike(filePath);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
return await this.parseGeneric(filePath);
|
|
474
|
+
}
|
|
405
475
|
}
|
|
406
|
-
|
|
407
|
-
|
|
476
|
+
catch (error) {
|
|
477
|
+
console.warn(`[Graph] Failed to parse ${filePath}:`, error instanceof Error ? error.message : String(error));
|
|
478
|
+
return { imports: [], symbols: [] };
|
|
408
479
|
}
|
|
409
480
|
}
|
|
410
481
|
async parseTypeScript(filePath) {
|
package/dist/http-server.js
CHANGED
|
@@ -35,7 +35,7 @@ const PORT = process.env.PORT || 3000;
|
|
|
35
35
|
const corsOrigin = process.env.CORS_ORIGIN;
|
|
36
36
|
let corsOptions = {
|
|
37
37
|
origin: false, // Default: deny all cross-origin requests
|
|
38
|
-
credentials:
|
|
38
|
+
credentials: false, // Must be false when origin is false
|
|
39
39
|
};
|
|
40
40
|
if (corsOrigin === '*') {
|
|
41
41
|
console.warn('⚠️ WARNING: CORS set to allow all origins (*). This is not secure for production!');
|
|
@@ -43,6 +43,7 @@ if (corsOrigin === '*') {
|
|
|
43
43
|
}
|
|
44
44
|
else if (corsOrigin) {
|
|
45
45
|
const allowedOrigins = corsOrigin.split(',').map(o => o.trim());
|
|
46
|
+
corsOptions.credentials = true; // Enable credentials for specific origins
|
|
46
47
|
corsOptions.origin = function (origin, callback) {
|
|
47
48
|
// Allow requests with no origin (like mobile apps, curl, Postman)
|
|
48
49
|
if (!origin)
|
|
@@ -64,6 +65,39 @@ app.use(cors(corsOptions));
|
|
|
64
65
|
*/
|
|
65
66
|
const bodyLimit = process.env.BODY_LIMIT || '10mb';
|
|
66
67
|
app.use(express.json({ limit: bodyLimit }));
|
|
68
|
+
// Request Logging Middleware
|
|
69
|
+
app.use((req, res, next) => {
|
|
70
|
+
const start = Date.now();
|
|
71
|
+
res.on('finish', () => {
|
|
72
|
+
const duration = Date.now() - start;
|
|
73
|
+
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
|
|
74
|
+
});
|
|
75
|
+
next();
|
|
76
|
+
});
|
|
77
|
+
// Rate Limiting Middleware
|
|
78
|
+
const requestCounts = new Map();
|
|
79
|
+
const RATE_LIMIT = 100; // requests per minute
|
|
80
|
+
const RATE_WINDOW = 60000; // 1 minute
|
|
81
|
+
app.use((req, res, next) => {
|
|
82
|
+
const clientIp = req.ip || req.connection.remoteAddress || 'unknown';
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
if (!requestCounts.has(clientIp)) {
|
|
85
|
+
requestCounts.set(clientIp, []);
|
|
86
|
+
}
|
|
87
|
+
const requests = requestCounts.get(clientIp);
|
|
88
|
+
// Remove old requests
|
|
89
|
+
while (requests.length > 0 && now - requests[0] > RATE_WINDOW) {
|
|
90
|
+
requests.shift();
|
|
91
|
+
}
|
|
92
|
+
if (requests.length >= RATE_LIMIT) {
|
|
93
|
+
return res.status(429).json({
|
|
94
|
+
error: 'Rate limit exceeded',
|
|
95
|
+
retryAfter: Math.ceil((requests[0] + RATE_WINDOW - now) / 1000)
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
requests.push(now);
|
|
99
|
+
next();
|
|
100
|
+
});
|
|
67
101
|
// Log security configuration on startup
|
|
68
102
|
console.log(`Security Configuration:`);
|
|
69
103
|
console.log(` CORS: ${corsOrigin === '*' ? 'OPEN (all origins)' : corsOrigin ? `Restricted to: ${corsOrigin}` : 'Same-origin only'}`);
|
|
@@ -108,12 +142,40 @@ const managers = ServerManagers.getInstance();
|
|
|
108
142
|
* HEALTH & METADATA ENDPOINTS
|
|
109
143
|
* ============================================================================
|
|
110
144
|
*/
|
|
111
|
-
app.get('/health', (req, res) => {
|
|
145
|
+
app.get('/health', async (req, res) => {
|
|
146
|
+
const memoryUsage = process.memoryUsage();
|
|
147
|
+
const uptime = process.uptime();
|
|
148
|
+
// Check disk space (if possible)
|
|
149
|
+
let diskInfo = null;
|
|
150
|
+
try {
|
|
151
|
+
const { execAsync } = await import('./utils.js');
|
|
152
|
+
const { stdout } = await execAsync('df -h .');
|
|
153
|
+
diskInfo = stdout.split('\n')[1]?.trim();
|
|
154
|
+
}
|
|
155
|
+
catch { }
|
|
112
156
|
res.json({
|
|
113
157
|
status: 'ok',
|
|
114
158
|
service: 'sequential-thinking-http-wrapper',
|
|
115
159
|
version: '2.0',
|
|
116
|
-
timestamp: new Date().toISOString()
|
|
160
|
+
timestamp: new Date().toISOString(),
|
|
161
|
+
system: {
|
|
162
|
+
memory: {
|
|
163
|
+
used: Math.round(memoryUsage.heapUsed / 1024 / 1024) + 'MB',
|
|
164
|
+
total: Math.round(memoryUsage.heapTotal / 1024 / 1024) + 'MB',
|
|
165
|
+
rss: Math.round(memoryUsage.rss / 1024 / 1024) + 'MB'
|
|
166
|
+
},
|
|
167
|
+
uptime: Math.floor(uptime) + 's',
|
|
168
|
+
disk: diskInfo
|
|
169
|
+
},
|
|
170
|
+
managers: {
|
|
171
|
+
thinking: {
|
|
172
|
+
historyLength: managers.thinking.getHistoryLength(),
|
|
173
|
+
currentBlock: managers.thinking.getCurrentBlock()?.id || null
|
|
174
|
+
},
|
|
175
|
+
notes: {
|
|
176
|
+
count: (await managers.notes.listNotes()).length
|
|
177
|
+
}
|
|
178
|
+
}
|
|
117
179
|
});
|
|
118
180
|
});
|
|
119
181
|
app.get('/', (req, res) => {
|
|
@@ -225,7 +287,10 @@ app.post('/api/thinking/thought', async (req, res) => {
|
|
|
225
287
|
*/
|
|
226
288
|
app.get('/api/thinking/history', async (req, res) => {
|
|
227
289
|
try {
|
|
228
|
-
const history = await managers.thinking.searchHistory('');
|
|
290
|
+
const history = await managers.thinking.searchHistory('') || [];
|
|
291
|
+
if (!Array.isArray(history)) {
|
|
292
|
+
return res.status(500).json({ error: 'Invalid history data' });
|
|
293
|
+
}
|
|
229
294
|
const transformedHistory = history.map((t, index) => ({
|
|
230
295
|
id: `thought-${index}-${t.thoughtNumber}`,
|
|
231
296
|
text: t.thought,
|
|
@@ -478,8 +543,13 @@ app.get('/api/notes', async (req, res) => {
|
|
|
478
543
|
try {
|
|
479
544
|
const tag = req.query.tag;
|
|
480
545
|
const includeExpired = req.query.includeExpired === 'true';
|
|
481
|
-
const
|
|
482
|
-
const
|
|
546
|
+
const page = parseInt(req.query.page) || 1;
|
|
547
|
+
const limit = Math.min(parseInt(req.query.limit) || 20, 100); // Max 100
|
|
548
|
+
const allNotes = await managers.notes.listNotes(tag, includeExpired);
|
|
549
|
+
const total = allNotes.length;
|
|
550
|
+
const offset = (page - 1) * limit;
|
|
551
|
+
const paginatedNotes = allNotes.slice(offset, offset + limit);
|
|
552
|
+
const transformedNotes = paginatedNotes.map(note => ({
|
|
483
553
|
id: note.id,
|
|
484
554
|
_id: note.id,
|
|
485
555
|
title: note.title,
|
|
@@ -491,7 +561,15 @@ app.get('/api/notes', async (req, res) => {
|
|
|
491
561
|
createdAt: note.createdAt,
|
|
492
562
|
updatedAt: note.updatedAt
|
|
493
563
|
}));
|
|
494
|
-
res.json(
|
|
564
|
+
res.json({
|
|
565
|
+
data: transformedNotes,
|
|
566
|
+
pagination: {
|
|
567
|
+
page,
|
|
568
|
+
limit,
|
|
569
|
+
total,
|
|
570
|
+
totalPages: Math.ceil(total / limit)
|
|
571
|
+
}
|
|
572
|
+
});
|
|
495
573
|
}
|
|
496
574
|
catch (error) {
|
|
497
575
|
res.status(500).json({
|
|
@@ -981,7 +1059,8 @@ app.get('/api/admin/stats', async (req, res) => {
|
|
|
981
1059
|
}
|
|
982
1060
|
});
|
|
983
1061
|
// Start server
|
|
984
|
-
|
|
1062
|
+
let isShuttingDown = false;
|
|
1063
|
+
const server = app.listen(PORT, () => {
|
|
985
1064
|
console.log(`Sequential Thinking HTTP Server running on port ${PORT}`);
|
|
986
1065
|
console.log(`Health check: http://localhost:${PORT}/health`);
|
|
987
1066
|
console.log(`API docs: http://localhost:${PORT}/`);
|
|
@@ -990,3 +1069,32 @@ app.listen(PORT, () => {
|
|
|
990
1069
|
console.log('This server uses in-memory storage. Do NOT run in cluster mode');
|
|
991
1070
|
console.log('without external storage (Redis/PostgreSQL) for data persistence.');
|
|
992
1071
|
});
|
|
1072
|
+
async function gracefulShutdown(signal) {
|
|
1073
|
+
if (isShuttingDown)
|
|
1074
|
+
return;
|
|
1075
|
+
isShuttingDown = true;
|
|
1076
|
+
console.log(`\n[${signal}] Starting graceful shutdown...`);
|
|
1077
|
+
// Stop accepting new connections
|
|
1078
|
+
server.close(async () => {
|
|
1079
|
+
console.log('[Shutdown] HTTP server closed');
|
|
1080
|
+
// Save all data
|
|
1081
|
+
try {
|
|
1082
|
+
await managers.thinking.gracefulShutdown?.();
|
|
1083
|
+
await managers.notes.save?.(); // Assuming save exists or will fail safely
|
|
1084
|
+
await managers.codeDb.save?.();
|
|
1085
|
+
console.log('[Shutdown] All data saved');
|
|
1086
|
+
}
|
|
1087
|
+
catch (error) {
|
|
1088
|
+
console.error('[Shutdown] Error saving data:', error);
|
|
1089
|
+
}
|
|
1090
|
+
console.log('[Shutdown] Complete');
|
|
1091
|
+
process.exit(0);
|
|
1092
|
+
});
|
|
1093
|
+
// Force shutdown after 10 seconds
|
|
1094
|
+
setTimeout(() => {
|
|
1095
|
+
console.error('[Shutdown] Forced exit after timeout');
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}, 10000);
|
|
1098
|
+
}
|
|
1099
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
1100
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
package/dist/lib.d.ts
CHANGED
|
@@ -40,7 +40,8 @@ export declare class SequentialThinkingServer {
|
|
|
40
40
|
private confidenceScore;
|
|
41
41
|
private contextManager;
|
|
42
42
|
private ruleManager;
|
|
43
|
-
|
|
43
|
+
private maxHistorySize;
|
|
44
|
+
constructor(storagePath?: string, delayMs?: number, maxHistorySize?: number);
|
|
44
45
|
private loadHistory;
|
|
45
46
|
private attemptRecovery;
|
|
46
47
|
private rebuildBlocks;
|
|
@@ -72,6 +73,7 @@ export declare class SequentialThinkingServer {
|
|
|
72
73
|
thoughtCount: number;
|
|
73
74
|
}[];
|
|
74
75
|
getHistoryLength(): number;
|
|
76
|
+
gracefulShutdown(): Promise<void>;
|
|
75
77
|
private learnFromFailures;
|
|
76
78
|
}
|
|
77
79
|
export {};
|
package/dist/lib.js
CHANGED
|
@@ -19,7 +19,9 @@ export class SequentialThinkingServer {
|
|
|
19
19
|
confidenceScore = 100; // Meta-Cognition Score (0-100)
|
|
20
20
|
contextManager = new ContextManager();
|
|
21
21
|
ruleManager = new RuleManager();
|
|
22
|
-
|
|
22
|
+
maxHistorySize;
|
|
23
|
+
constructor(storagePath = 'thoughts_history.json', delayMs = 0, maxHistorySize = 1000) {
|
|
24
|
+
this.maxHistorySize = maxHistorySize;
|
|
23
25
|
this.disableThoughtLogging = (process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true";
|
|
24
26
|
this.storagePath = path.resolve(storagePath);
|
|
25
27
|
this.solutionsDbPath = path.resolve(path.dirname(this.storagePath), 'solutions_db.json');
|
|
@@ -190,7 +192,13 @@ export class SequentialThinkingServer {
|
|
|
190
192
|
try {
|
|
191
193
|
// Auto-Pruning: Keep max 100 thoughts, summarize older blocks
|
|
192
194
|
if (this.thoughtHistory.length > 100) {
|
|
193
|
-
|
|
195
|
+
try {
|
|
196
|
+
this.autoPrune();
|
|
197
|
+
}
|
|
198
|
+
catch (pruneError) {
|
|
199
|
+
console.error('[AutoPrune] Failed:', pruneError);
|
|
200
|
+
// Continue saving without pruning
|
|
201
|
+
}
|
|
194
202
|
}
|
|
195
203
|
const storage = {
|
|
196
204
|
version: '2.0',
|
|
@@ -199,10 +207,21 @@ export class SequentialThinkingServer {
|
|
|
199
207
|
};
|
|
200
208
|
const tmpPath = `${this.storagePath}.tmp`;
|
|
201
209
|
await fs.writeFile(tmpPath, JSON.stringify(storage, null, 2), 'utf-8');
|
|
210
|
+
// Verify file was written correctly
|
|
211
|
+
const stats = await fs.stat(tmpPath);
|
|
212
|
+
if (stats.size === 0) {
|
|
213
|
+
throw new Error('Temporary file is empty after write');
|
|
214
|
+
}
|
|
202
215
|
await fs.rename(tmpPath, this.storagePath);
|
|
203
216
|
}
|
|
204
217
|
catch (error) {
|
|
205
218
|
console.error(`Error saving history to ${this.storagePath}:`, error);
|
|
219
|
+
// Cleanup temp file if exists
|
|
220
|
+
try {
|
|
221
|
+
await fs.unlink(`${this.storagePath}.tmp`);
|
|
222
|
+
}
|
|
223
|
+
catch { }
|
|
224
|
+
throw error; // Re-throw to let caller know
|
|
206
225
|
}
|
|
207
226
|
});
|
|
208
227
|
}
|
|
@@ -331,6 +350,12 @@ export class SequentialThinkingServer {
|
|
|
331
350
|
if (input.thoughtNumber > input.totalThoughts) {
|
|
332
351
|
input.totalThoughts = input.thoughtNumber;
|
|
333
352
|
}
|
|
353
|
+
// Enforce max history size
|
|
354
|
+
if (this.thoughtHistory.length >= this.maxHistorySize) {
|
|
355
|
+
const removeCount = this.thoughtHistory.length - this.maxHistorySize + 1;
|
|
356
|
+
this.thoughtHistory.splice(0, removeCount);
|
|
357
|
+
console.warn(`[Memory] Removed ${removeCount} old thoughts to maintain limit of ${this.maxHistorySize}`);
|
|
358
|
+
}
|
|
334
359
|
this.thoughtHistory.push(input);
|
|
335
360
|
if (input.branchFromThought && input.branchId) {
|
|
336
361
|
const branchKey = `${input.branchFromThought}-${input.branchId}`;
|
|
@@ -671,8 +696,9 @@ ${typeof wrappedThought === 'string' && wrappedThought.startsWith('│') ? wrapp
|
|
|
671
696
|
this.consecutiveStallCount = 0;
|
|
672
697
|
}
|
|
673
698
|
// --- 📊 Update Confidence Score ---
|
|
674
|
-
if (warnings.length > 0)
|
|
675
|
-
this.confidenceScore
|
|
699
|
+
if (warnings.length > 0) {
|
|
700
|
+
this.confidenceScore = Math.max(0, this.confidenceScore - (5 * warnings.length));
|
|
701
|
+
}
|
|
676
702
|
if (input.thoughtType === 'reflexion') {
|
|
677
703
|
this.confidenceScore = Math.min(100, this.confidenceScore + 10);
|
|
678
704
|
}
|
|
@@ -819,6 +845,16 @@ ${typeof wrappedThought === 'string' && wrappedThought.startsWith('│') ? wrapp
|
|
|
819
845
|
getHistoryLength() {
|
|
820
846
|
return this.thoughtHistory.length;
|
|
821
847
|
}
|
|
848
|
+
async gracefulShutdown() {
|
|
849
|
+
console.log('[Shutdown] Saving thought history...');
|
|
850
|
+
try {
|
|
851
|
+
await this.saveHistory();
|
|
852
|
+
console.log('[Shutdown] History saved successfully');
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
console.error('[Shutdown] Failed to save history:', error);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
822
858
|
async learnFromFailures(blockId) {
|
|
823
859
|
const blockThoughts = this.thoughtHistory.filter(t => t.blockId === blockId);
|
|
824
860
|
for (let i = 0; i < blockThoughts.length - 1; i++) {
|
package/dist/notes.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export declare class NotesManager {
|
|
|
33
33
|
* Atomic save using write-to-temp + rename pattern.
|
|
34
34
|
* This ensures either the old file or new file exists, never a partial write.
|
|
35
35
|
*/
|
|
36
|
-
|
|
36
|
+
save(): Promise<void>;
|
|
37
37
|
/**
|
|
38
38
|
* Reload notes from disk (useful for external changes)
|
|
39
39
|
*/
|
package/dist/tools/coding.js
CHANGED
|
@@ -93,6 +93,17 @@ export function registerCodingTools(server, graph) {
|
|
|
93
93
|
}, async ({ path: filePath, oldText, newText, reasoning, runTest }) => {
|
|
94
94
|
try {
|
|
95
95
|
const absolutePath = validatePath(filePath);
|
|
96
|
+
const stats = await fs.stat(absolutePath);
|
|
97
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
98
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
99
|
+
return {
|
|
100
|
+
content: [{
|
|
101
|
+
type: "text",
|
|
102
|
+
text: `Error: File size (${Math.round(stats.size / 1024 / 1024)}MB) exceeds maximum allowed (${MAX_FILE_SIZE / 1024 / 1024}MB)`
|
|
103
|
+
}],
|
|
104
|
+
isError: true
|
|
105
|
+
};
|
|
106
|
+
}
|
|
96
107
|
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
97
108
|
if (!content.includes(oldText)) {
|
|
98
109
|
return { content: [{ type: "text", text: "Error: Target text not found. Ensure exact match including whitespace." }], isError: true };
|
|
@@ -159,10 +170,16 @@ export function registerCodingTools(server, graph) {
|
|
|
159
170
|
}
|
|
160
171
|
}
|
|
161
172
|
// -------------------------------------
|
|
173
|
+
// Cleanup backup file after successful edit (keep only last 10)
|
|
174
|
+
await cleanupBackups(path.dirname(absolutePath), 10);
|
|
175
|
+
try {
|
|
176
|
+
await fs.unlink(backupPath);
|
|
177
|
+
}
|
|
178
|
+
catch { }
|
|
162
179
|
return {
|
|
163
180
|
content: [{
|
|
164
181
|
type: "text",
|
|
165
|
-
text: `Successfully applied edit to ${filePath}.\nReasoning verified: ${reasoning}
|
|
182
|
+
text: `Successfully applied edit to ${filePath}.\nReasoning verified: ${reasoning}`
|
|
166
183
|
}]
|
|
167
184
|
};
|
|
168
185
|
}
|
|
@@ -171,3 +188,28 @@ export function registerCodingTools(server, graph) {
|
|
|
171
188
|
}
|
|
172
189
|
});
|
|
173
190
|
}
|
|
191
|
+
async function cleanupBackups(dir, maxBackups = 10) {
|
|
192
|
+
try {
|
|
193
|
+
const files = await fs.readdir(dir);
|
|
194
|
+
const backupFiles = await Promise.all(files
|
|
195
|
+
.filter(f => f.endsWith('.bak'))
|
|
196
|
+
.map(async (f) => ({
|
|
197
|
+
name: f,
|
|
198
|
+
path: path.join(dir, f),
|
|
199
|
+
stat: await fs.stat(path.join(dir, f))
|
|
200
|
+
})));
|
|
201
|
+
// Sort by mtime (newest first)
|
|
202
|
+
const sortedBackups = backupFiles.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
|
|
203
|
+
// Remove old backups
|
|
204
|
+
for (let i = maxBackups; i < sortedBackups.length; i++) {
|
|
205
|
+
try {
|
|
206
|
+
await fs.unlink(sortedBackups[i].path);
|
|
207
|
+
console.log(`[Backup] Cleaned up old backup: ${sortedBackups[i].name}`);
|
|
208
|
+
}
|
|
209
|
+
catch { }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// Ignore cleanup errors
|
|
214
|
+
}
|
|
215
|
+
}
|
package/dist/tools/web.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
export declare function scrapeWebpage(url: string): Promise<string>;
|
|
2
|
+
export declare function scrapeWebpage(url: string, timeoutMs?: number): Promise<string>;
|
|
3
3
|
export declare function registerWebTools(server: McpServer): void;
|
package/dist/tools/web.js
CHANGED
|
@@ -3,65 +3,100 @@ import { fetchWithRetry, validatePublicUrl } from "../utils.js";
|
|
|
3
3
|
import { JSDOM } from 'jsdom';
|
|
4
4
|
import { Readability } from '@mozilla/readability';
|
|
5
5
|
import TurndownService from 'turndown';
|
|
6
|
-
|
|
6
|
+
class SearchRateLimiter {
|
|
7
|
+
requests = new Map(); // provider -> timestamps[]
|
|
8
|
+
limits = {
|
|
9
|
+
brave: { requests: 100, windowMs: 60000 }, // 100/min
|
|
10
|
+
exa: { requests: 50, windowMs: 60000 }, // 50/min
|
|
11
|
+
google: { requests: 100, windowMs: 60000 }, // 100/min
|
|
12
|
+
duckduckgo: { requests: 30, windowMs: 60000 } // 30/min (be nice to DDG)
|
|
13
|
+
};
|
|
14
|
+
canProceed(provider) {
|
|
15
|
+
const limit = this.limits[provider];
|
|
16
|
+
if (!limit)
|
|
17
|
+
return true;
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
const timestamps = this.requests.get(provider) || [];
|
|
20
|
+
// Remove old requests outside window
|
|
21
|
+
const validTimestamps = timestamps.filter((t) => now - t < limit.windowMs);
|
|
22
|
+
this.requests.set(provider, validTimestamps);
|
|
23
|
+
return validTimestamps.length < limit.requests;
|
|
24
|
+
}
|
|
25
|
+
recordRequest(provider) {
|
|
26
|
+
const timestamps = this.requests.get(provider) || [];
|
|
27
|
+
timestamps.push(Date.now());
|
|
28
|
+
this.requests.set(provider, timestamps);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const rateLimiter = new SearchRateLimiter();
|
|
32
|
+
export async function scrapeWebpage(url, timeoutMs = 30000) {
|
|
7
33
|
await validatePublicUrl(url);
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetchWithRetry(url, {
|
|
38
|
+
signal: controller.signal
|
|
39
|
+
});
|
|
40
|
+
const html = await response.text();
|
|
41
|
+
const doc = new JSDOM(html, { url });
|
|
42
|
+
const reader = new Readability(doc.window.document);
|
|
43
|
+
const article = reader.parse();
|
|
44
|
+
if (!article)
|
|
45
|
+
throw new Error("Could not parse article content");
|
|
46
|
+
const turndownService = new TurndownService({
|
|
47
|
+
headingStyle: 'atx',
|
|
48
|
+
codeBlockStyle: 'fenced'
|
|
49
|
+
});
|
|
50
|
+
// Custom Rule for GitHub Flavored Markdown Tables
|
|
51
|
+
turndownService.addRule('tables', {
|
|
52
|
+
filter: ['table'],
|
|
53
|
+
replacement: function (content, node) {
|
|
54
|
+
const rows = [];
|
|
55
|
+
const table = node;
|
|
56
|
+
const trs = Array.from(table.querySelectorAll('tr'));
|
|
57
|
+
trs.forEach((tr, index) => {
|
|
58
|
+
const cols = [];
|
|
59
|
+
const tds = Array.from(tr.querySelectorAll('th, td'));
|
|
60
|
+
tds.forEach(td => {
|
|
61
|
+
// Clean content: remove newlines and pipe characters
|
|
62
|
+
cols.push(td.textContent?.replace(/[\n\r]/g, ' ').replace(/\|/g, '\\|').trim() || "");
|
|
63
|
+
});
|
|
64
|
+
if (cols.length > 0) {
|
|
65
|
+
rows.push(`| ${cols.join(' | ')} |`);
|
|
66
|
+
// Add separator after header
|
|
67
|
+
if (index === 0 || tr.querySelector('th')) {
|
|
68
|
+
rows.push(`| ${cols.map(() => '---').join(' | ')} |`);
|
|
69
|
+
}
|
|
38
70
|
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
71
|
+
});
|
|
72
|
+
// Filter out duplicate separator lines if any
|
|
73
|
+
const uniqueRows = rows.filter((row, i) => {
|
|
74
|
+
if (row.includes('---') && rows[i - 1]?.includes('---'))
|
|
75
|
+
return false;
|
|
76
|
+
return true;
|
|
77
|
+
});
|
|
78
|
+
return '\n\n' + uniqueRows.join('\n') + '\n\n';
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
let markdown = turndownService.turndown(article.content || "");
|
|
82
|
+
if (markdown.length > 20000) {
|
|
83
|
+
markdown = markdown.substring(0, 20000) + "\n...(truncated)";
|
|
48
84
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
85
|
+
return `Title: ${article.title}\n\n${markdown}`;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
clearTimeout(timeoutId);
|
|
53
89
|
}
|
|
54
|
-
return `Title: ${article.title}\n\n${markdown}`;
|
|
55
90
|
}
|
|
56
91
|
export function registerWebTools(server) {
|
|
57
92
|
// 1. web_search
|
|
58
93
|
server.tool("web_search", "Search the web using Brave or Exa APIs (requires API keys in environment variables: BRAVE_API_KEY or EXA_API_KEY).", {
|
|
59
94
|
query: z.string().min(1).describe("The search query"),
|
|
60
|
-
provider: z.enum(['brave', 'exa', 'google']).optional().describe("Preferred search provider")
|
|
95
|
+
provider: z.enum(['brave', 'exa', 'google', 'duckduckgo']).optional().describe("Preferred search provider")
|
|
61
96
|
}, async ({ query, provider }) => {
|
|
62
97
|
const errors = [];
|
|
63
98
|
// 1. Identify available providers
|
|
64
|
-
const availableProviders = [];
|
|
99
|
+
const availableProviders = ['duckduckgo']; // Always available (HTML scraping)
|
|
65
100
|
if (process.env.BRAVE_API_KEY)
|
|
66
101
|
availableProviders.push('brave');
|
|
67
102
|
if (process.env.EXA_API_KEY)
|
|
@@ -91,7 +126,45 @@ export function registerWebTools(server) {
|
|
|
91
126
|
// Default priority if no preference: Brave > Exa > Google (Already respected by push order if mapped correctly, but let's be explicit if needed. Here they are added in that order.)
|
|
92
127
|
// 3. Try providers sequentially
|
|
93
128
|
for (const currentProvider of attemptOrder) {
|
|
129
|
+
if (!rateLimiter.canProceed(currentProvider)) {
|
|
130
|
+
errors.push(`[${currentProvider}] Rate limit exceeded`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
rateLimiter.recordRequest(currentProvider);
|
|
94
134
|
try {
|
|
135
|
+
if (currentProvider === 'duckduckgo') {
|
|
136
|
+
try {
|
|
137
|
+
// DuckDuckGo HTML interface (no API key required)
|
|
138
|
+
const response = await fetchWithRetry(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`, {
|
|
139
|
+
headers: {
|
|
140
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
if (!response.ok)
|
|
144
|
+
throw new Error(`DuckDuckGo Status ${response.status}`);
|
|
145
|
+
const html = await response.text();
|
|
146
|
+
// Simple regex extraction (in production, use proper HTML parser)
|
|
147
|
+
const results = [];
|
|
148
|
+
const resultRegex = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/g;
|
|
149
|
+
let match;
|
|
150
|
+
while ((match = resultRegex.exec(html)) !== null && results.length < 5) {
|
|
151
|
+
results.push({
|
|
152
|
+
title: match[2].replace(/<[^>]*>/g, ''), // Strip HTML tags
|
|
153
|
+
url: match[1],
|
|
154
|
+
snippet: ''
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
content: [{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: JSON.stringify(results, null, 2)
|
|
161
|
+
}]
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
throw error; // Let outer loop handle it
|
|
166
|
+
}
|
|
167
|
+
}
|
|
95
168
|
if (currentProvider === 'brave') {
|
|
96
169
|
const response = await fetchWithRetry(`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=5`, {
|
|
97
170
|
headers: {
|
package/dist/utils.d.ts
CHANGED
|
@@ -51,4 +51,13 @@ export declare const DEFAULT_HEADERS: {
|
|
|
51
51
|
'Accept-Language': string;
|
|
52
52
|
};
|
|
53
53
|
export declare function fetchWithRetry(url: string, options?: any, retries?: number, backoff?: number): Promise<Response>;
|
|
54
|
+
export declare function withRetry(fn: () => Promise<any>, maxRetries?: number, baseDelay?: number): Promise<any>;
|
|
55
|
+
export declare class FileLock {
|
|
56
|
+
locks: Map<any, any>;
|
|
57
|
+
acquire(filePath: string, timeout?: number): Promise<{
|
|
58
|
+
release: () => void;
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
export declare const fileLock: FileLock;
|
|
62
|
+
export declare function sanitizeInput(input: any, maxLength?: number): string;
|
|
54
63
|
export {};
|
package/dist/utils.js
CHANGED
|
@@ -9,7 +9,7 @@ import chalk from 'chalk';
|
|
|
9
9
|
* Shell metacharacters that could enable command injection
|
|
10
10
|
* Blocking these prevents command chaining like: cmd; rm -rf /
|
|
11
11
|
*/
|
|
12
|
-
const SHELL_METACHARACTERS = /[;|&`$()
|
|
12
|
+
const SHELL_METACHARACTERS = /[;|&`$()<>\{\}\[\]\*\?~]|\$\{[^}]*\}|\\x[0-9a-fA-F]{2}|\$[a-zA-Z_][a-zA-Z0-9_]*/gu;
|
|
13
13
|
/**
|
|
14
14
|
* Whitelist of safe commands that can be executed
|
|
15
15
|
* Commands must be explicitly allowed for security
|
|
@@ -118,6 +118,13 @@ function validateCommand(command) {
|
|
|
118
118
|
new RegExp('chown\\s+-R\\s+root:'), // Recursive ownership change
|
|
119
119
|
new RegExp('fdisk'), // Partition tools
|
|
120
120
|
new RegExp('>\\s*/dev/\\w+\\s*\\+w'), // Write to device
|
|
121
|
+
// New patterns:
|
|
122
|
+
new RegExp('curl\\s+.*\\s*\\|\\s*sh'), // curl | sh
|
|
123
|
+
new RegExp('wget\\s+.*\\s*\\|\\s*sh'), // wget | sh
|
|
124
|
+
new RegExp('base64\\s+-d\\s*\\|'), // encoded commands
|
|
125
|
+
new RegExp('eval\\s*\\('), // eval()
|
|
126
|
+
new RegExp('\\$\\(\\(.*\\)\\)'), // $() command substitution
|
|
127
|
+
new RegExp('`.*`'), // backtick substitution
|
|
121
128
|
];
|
|
122
129
|
for (const pattern of dangerousPatterns) {
|
|
123
130
|
if (pattern.test(command)) {
|
|
@@ -418,3 +425,59 @@ export async function fetchWithRetry(url, options = {}, retries = 3, backoff = 1
|
|
|
418
425
|
throw error;
|
|
419
426
|
}
|
|
420
427
|
}
|
|
428
|
+
export async function withRetry(fn, maxRetries = 3, baseDelay = 100) {
|
|
429
|
+
let lastError;
|
|
430
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
431
|
+
try {
|
|
432
|
+
return await fn();
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
lastError = error;
|
|
436
|
+
// Don't retry on certain errors
|
|
437
|
+
if (error.code === 'ENOENT' || error.code === 'EACCES' || error.code === 'EPERM') {
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
if (i < maxRetries - 1) {
|
|
441
|
+
const delay = baseDelay * Math.pow(2, i);
|
|
442
|
+
console.warn(`[Retry] Attempt ${i + 1} failed, retrying in ${delay}ms...`);
|
|
443
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
throw lastError;
|
|
448
|
+
}
|
|
449
|
+
export class FileLock {
|
|
450
|
+
locks = new Map();
|
|
451
|
+
async acquire(filePath, timeout = 5000) {
|
|
452
|
+
const startTime = Date.now();
|
|
453
|
+
while (this.locks.has(filePath)) {
|
|
454
|
+
if (Date.now() - startTime > timeout) {
|
|
455
|
+
throw new Error(`Timeout acquiring lock for ${filePath}`);
|
|
456
|
+
}
|
|
457
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
458
|
+
}
|
|
459
|
+
this.locks.set(filePath, true);
|
|
460
|
+
return {
|
|
461
|
+
release: () => {
|
|
462
|
+
this.locks.delete(filePath);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
export const fileLock = new FileLock();
|
|
468
|
+
export function sanitizeInput(input, maxLength = 10000) {
|
|
469
|
+
if (typeof input !== 'string') {
|
|
470
|
+
return '';
|
|
471
|
+
}
|
|
472
|
+
// Trim whitespace
|
|
473
|
+
let sanitized = input.trim();
|
|
474
|
+
// Limit length
|
|
475
|
+
if (sanitized.length > maxLength) {
|
|
476
|
+
sanitized = sanitized.substring(0, maxLength);
|
|
477
|
+
}
|
|
478
|
+
// Remove null bytes
|
|
479
|
+
sanitized = sanitized.replace(/\x00/g, '');
|
|
480
|
+
// Normalize unicode (prevent homograph attacks)
|
|
481
|
+
sanitized = sanitized.normalize('NFC');
|
|
482
|
+
return sanitized;
|
|
483
|
+
}
|